#Task-14 水平捲動的 Collection View

利用 collection view 實現水平捲動的頁面

接續上一篇的 Line App 介面,首頁推薦的主題跟貼圖等都是用水平捲動的 view 來呈現,可以用 Scroll View 或是 Collection View 來達成,這次就使用 Collection View 來作出我們要的畫面吧!

介面跟資料建立的部分可以參考上一篇:

⭐️ 完成畫面如下:

畫面主要是由 Table View Controller 來建構,其中一個 Section 的 Cell 中放入 Collection View 來製作出水平捲動的效果,上面看到的三個可以水平捲動的區塊,其實都是用同一個 Collection View 做出來的哦!

如果是用 Scroll View 來做,就要用三個 Scroll View 搭配各自的元件來達成,相較之下比較麻煩一點,只是 Collection View 就要寫一些程式啦!

⭐️ 製作水平捲動畫面

1️⃣ 新增 Collection View 到 Table View Cell 底下,設定 Collection View 上下左右到 Content View 的 Auto Layout;Cell 大小設為 120 * 180,Cell 間的間距為 0

2️⃣ Collection View Cell 裡放 Image View 跟 Label,將他們裝進 Stack View 中,讓 Stack View 跟 Cell 上下左右對齊,並依需求設定 Image View 跟 Label 的 Auto Layout

3️⃣ 將 Table View Controller 設為 Collection View 的 Data Source 跟 Delegate

4️⃣ 新增 Cocoa Touch Class 的 UICollectionViewCell 類別,連結 Image View 跟 Label 的 outlet;設定 Collection View Cell 的 Class 爲剛剛建立的類別,並設定 Reusable Identifier

class serviceCollectionViewCell: UICollectionViewCell {

static let reuseIdentifier = "\(serviceCollectionViewCell.self)"

@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!

}

5️⃣ 讓 Table View Controller 遵從 UICollectionViewDelegate、 UICollectionViewDataSource Protocol,連結 collection view 的 outlet,並設定 Section 跟 cell 的數量

class mainTableViewController: UITableViewController, UICollectionViewDelegate, UICollectionViewDataSource {

@IBOutlet weak var serviceCollectionView: UICollectionView!
//設定Section數量,這裡需要3個,回傳3
func numberOfSections(in serviceCollectionView: UICollectionView) -> Int {
return 3
}

//設定3個Section個別需要幾個cell,回傳對應的數量值
func collectionView(_ serviceCollectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

if section == 0{
return 6
}else if section == 1{
return 10
}else{
return 10
}

}
}

6️⃣ 設定每個 Section 中對應的 Cell 內容

func collectionView(_ serviceCollectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

//轉型
let cell = serviceCollectionView.dequeueReusableCell(withReuseIdentifier: "\(serviceCollectionViewCell.self)", for: indexPath) as! serviceCollectionViewCell

//設定不同Section對應要顯示的資料內容(資料建立的部分請參考上一篇文章)
if indexPath.section == 0{
cell.imageView.image = UIImage(named: services[indexPath.item].image)
cell.nameLabel.text = services[indexPath.item].name
}else if indexPath.section == 1{
cell.imageView.image = UIImage(named: themes[indexPath.item].image)
cell.nameLabel.text = themes[indexPath.item].name
}else{
cell.imageView.image = UIImage(named: stickers[indexPath.item].image)
cell.nameLabel.text = stickers[indexPath.item].name
}
return cell
}

7️⃣ 使用 Compositional Layout 排版 ⭐️⭐️⭐️

Collection View 預設的 Layout 是 Flow,這次我們要用更方便排版的 Compositional Layout 來製作畫面!

Compositional Layout 比起 Flow Layout,多了 Group 來協助佈置 Item 的排列,讓排版方式變得更彈性,也能做出更複雜的排版

各式的排版方式可以參考彼得潘的文章:

  • 產生不同的 NSCollectionLayoutSection,讓每個 Section 採用不同的排版方式
//服務區塊
var serviceSection: NSCollectionLayoutSection {

//設定Item、Group、Section大小
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(90), heightDimension: .absolute(90))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(540), heightDimension: .absolute(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)

//設定Section可以持續捲動,使用orthogonalScrollingBehavior,Section的捲動方向將與Collection View原本的捲動方向垂直,在這邊就變成水平捲動
section.orthogonalScrollingBehavior = .continuous
return section

}
//主題區塊
var themeSection: NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(120), heightDimension: .absolute(180))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(1200), heightDimension: .absolute(210))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
}
//貼圖區塊
var stickerSection: NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .absolute(170))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(1100), heightDimension: .absolute(170))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
}

每個 Section 需要設定 Item、Group、Section 的大小,這邊我都是使用固定大小的 absolute,不過裡面常用的有三種:

(1) absolute:固定的寬跟高

(2) fractionalWidth & fractionalHeight:依照比例設定寬跟高
例如我們裝 Item 的 Group 大小是 100* 80,那 fractionalWidth(0.5) 及 fractionalHeight(1) 的 Item 寬就是 50,高是80,所以 Group 裡面可以裝水平的 2 個 Item

(3) estimated:給予寬跟高的初始估計值,會在執行時自動計算 Item、Group 在這個值內最合適的大小。適合用在內容大小在執行時可能發生變化的時候

  • 定義呼叫到哪個 Section 時該回傳的 computed property,並在 viewDidLoad 中將 collectionViewLayout 設為 UICollectionViewCompositionalLayout
    override func viewDidLoad() {
super.viewDidLoad()
serviceCollectionView.collectionViewLayout = generateLayout()
}
func generateLayout() -> UICollectionViewLayout{
UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
if sectionIndex == 0{
return self.serviceSection
}else if sectionIndex == 1{
return self.themeSection
}else{
return self.stickerSection
}
}
}

彼得潘有提到:在產生 UICollectionViewCompositionalLayout 時必須加上 capture list [unowned self],不然會產生 reference cycle 的記憶體問題!

這時候執行看看,應該已經有大致上完成我們要的畫面了!有出現 3 個 Section、對應的內容跟可以水平捲動,只剩下 Section 左上方的 Header 標題了!

8️⃣ 加上 Header

  • 點選 Collection View,勾選 Accessories 的 Section Header,會看到本來的 Collection View 上方長出一條格子,列表中出現 Collection Reusable View 在 Collection View 底下,調整好需要的欄位大小後,將要用來顯示標題的 Label 放進去
  • 設定 Collection Reusable View 的 Reuse Identifier,新增一個 Cocoa Touch Class 的 UICollectionReusableView 類別,讓 Collection Reusable View 的 Class 設為該類別,並連結 Label 的 outlet
class headerView: UICollectionReusableView {

@IBOutlet weak var header: UILabel!

}
  • 宣告 Array 儲存 Header 名稱
//mainTableViewController    var headers = ["服務", "您可能喜歡的主題", "您可能喜歡的貼圖"]
  • 定義UICollectionViewDataSource 回傳 header 的 function
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

//轉型
let headerView = serviceCollectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! headerView

//設定Header文字
headerView.header.text = "\(headers[indexPath.section])"

return headerView
}
  • 設定 Header 的 Size

我們回到剛剛產生 NSCollectionLayoutSection 的程式碼,在裡面補上設定 Header 程式。這裡會用到 NSCollectionLayoutBoundarySupplementaryItem,專門用來在 Collection View 加上 Header 或 Footer 的物件

//3個Section都要補上,這裡以第一個當例子    var serviceSection: NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(90), heightDimension: .absolute(90))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(540), heightDimension: .absolute(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous

//設定Header大小
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(40))

//生成NSCollectionLayoutBoundarySupplementaryItem,Header內容靠右上對齊
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)

//設定Section的boundarySupplementaryItems
section.boundarySupplementaryItems = [header]


return section
}

這樣就完成啦!👏

GitHub 連結

參考資料

--

--