用 flow layout 的 collection view 實現水平滑動的分頁畫面
開發 iOS App 時,我們時常看到水平滑動的分頁設計,就像下圖 Developer App 的紅色框框區塊。
開發 UIKit App 時,我們可用 4 種方法實現這樣的功能。
- flow layout 的 collection view
- compositional layout 的 collection view
- scroll view
- page view controller
接下來我們將以 flow layout 的 collection view 為例,說明如何實現這樣的功能。
開發重點
使用 flow layout 的 collection view 實現水平滑動的分頁畫面時,關鍵在於以下 2 點:
- 設定 collection view 捲動方向為水平,並以分頁模式捲動。
- 設定 cell 的大小等於 collection view 的大小。
為了讓 cell 的大小等於 collection view 的大小,我們可在以下三種地方設定。
- 方法1: 定義 UICollectionViewDelegateFlowLayout 的 function collectionView(_:layout:sizeForItemAt:)。
- 方法2: 在 controller 的 viewDidLayoutSubviews 設定 UICollectionViewFlowLayout 的 itemSize。
- 方法3: 在 controller 的 viewDidAppear 設定 UICollectionViewFlowLayout 的 itemSize。
加入 collection view,設定 auto layout 條件
collection view 跟 Safe Area 的 top,leading & trailing 間距 10,collection view 的 aspect ratio 1:1。
設定 collection view 的 Scroll Direction 為 Horizontal,勾選 Paging Enabled
設定 collection view 的 dataSource & delegate 是 View Controller
設定 collection view cell
在 cell 裡加入 image view,image view 跟 content view 的邊界對齊,image view 的 Content Mode 設為 Aspect Fill。
新增繼承 UICollectionViewCell 的 NumberCollectionViewCell,將 cell 的類別和 Reuse ID 設為 NumberCollectionViewCell ,連結 outlet imageView & 宣告 cell 的 reuseIdentifier。
class NumberCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "\(NumberCollectionViewCell.self)"
@IBOutlet weak var imageView: UIImageView!
}
collection view 的 Estimate Size 設為 None,Min Spacing 設為 0
我們待會將從程式設定 collection view cell 的尺寸等於 collection view,因此 Estimate Size 要設為 None,否則它將用 auto layout 的條件計算 cell 的大小。
宣告儲存資料的 array
宣告 array photos 儲存 SF Symbol 的圖片名稱,從 1.circle.fill ~ 10.circle.fill。
class ViewController: UIViewController {
let photos = (1...10).map { "\($0).circle.fill" }
定義 UICollectionViewDataSource 的 function,設定 cell 的數量和內容
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
photos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NumberCollectionViewCell.reuseIdentifier, for: indexPath) as! NumberCollectionViewCell
let photo = photos[indexPath.item]
cell.imageView.image = UIImage(systemName: photo)
return cell
}
}
一頁顯示一個 cell,設定 cell 的尺寸等於 collection view
當 collection view 設為 Paging Enabled 時,分頁的尺寸將是 collection view 的尺寸。因此我們必須讓 cell 的尺寸等於 collection view 的尺寸,如此才能一頁顯示一個 cell,實現水平滑動的分頁效果。
由於 ViewController 是 collection view 的 delegate,因此 collection view 將透過 protocol UICollectionViewDelegateFlowLayout 的 function collectionView(_:layout:sizeForItemAt:) 決定 cell 的尺寸。我們回傳 collectionView.bounds.size,因此 cell 的尺寸將等於 collection view。
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.bounds.size
}
}
結果
其它例子: 換成比較漂亮的圖片。
範例連結
設定 cell 尺寸的其它方法
剛剛的例子主要利用 UICollectionViewDelegateFlowLayout 的 collectionView(_:layout:sizeForItemAt:) 設定 cell 尺寸,不過我們也可以不透過 delegate,而是在 controller 的 viewDidLayoutSubviews 或 viewDidAppear 設定 UICollectionViewFlowLayout 的 itemSize。
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let photos = (1...10).map { "\($0).circle.fill" }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout?.itemSize = collectionView?.bounds.size ?? .zero
layout?.estimatedItemSize = .zero
}
說明
layout?.itemSize = collectionView?.bounds.size
將讓 cell 的尺寸等於 collection view,layout?.estimatedItemSize = .zero
則表示不使用 auto layout 計算 cell 的尺寸。值得注意的,以上程式若改在 viewDidLoad 或 viewWillAppear 設定 cell 尺寸,將產生問題,因為 viewDidLoad 或 viewWillAppear 時 collection view 的大小是不正確的,要等到 viewDidLayoutSubviews 或 viewDidAppear 才能取得 collection view 經過 auto layout 計算後真正的大小。