利用 flow layout 的 collection view 製作照片牆
collection view 的 flow layout 很適合設計 iOS App 上常見的照片牆頁面,比方以下的 IG 頁面。
接下來就讓我們利用 flow layout 的 collection view 製作人見人愛的小王子照片牆吧。
Assets.xcassets 加入小王子的可愛圖片
加入 collection view controller
從下圖可看出 collection view 預設的 layout 為 flow layout。
collection view 的另一種 layout 是 custom layout,利用它可以做出各種特別的排版,不只是格子狀排列,不過它需要自己額外撰寫排版的程式。
對 custom layout 有興趣的朋友可參考以下 uicollectionview-layouts-kit 示範的範例。
cell 裡加入圖片,設定 auto layout 條件
在 cell 裡加入 image view。
將 image view 的圖片設為 pic0,Content Mode 設為 Aspect Fill。
依據是否學過 auto layout,分成兩種做法。
- 尚未學習 auto layout。
將 image view 設成跟 cell 一樣大,不用設定 auto layout。此做法無法滿足各種 iPhone 機型。
- 學過 auto layout。
點取 image view & content view,設定上下左右對齊,讓 image view 跟 cell 一樣大。
由於 collection view cell 預設將由 auto layout 條件推算大小,因此如上圖所示,此時 cell 將自動變成剛好容納小王子圖片的大小。
我們放在 cell 裡的小王子圖片是 280 * 420,因此 storyboard 裡的 cell 也變成 280 * 420。當然這不是我們想要的 cell 大小,關於 cell 大小的調整,我們等下會再做說明。
產生自訂的 collection view controller & collection view cell 類別
產生 PrinceCollectionViewController。
產生 PrinceCollectionViewCell。
將 collection view controller & collection view cell 的類別設為自訂類別
回到 Interface Builder,將 collection view controller & collection view cell 的類別分別設為 PrinceCollectionViewController & PrinceCollectionViewCell。
設定 collection view 的 data source &遵從 protocol UICollectionViewDataSource
UICollectionViewController 原本就遵從 protocol UICollectionViewDataSource,而且已被設為 collection view 的 data source,所以這一步我們可跳過。若是自己另外將 collection view 加到一般的 view controller 上,則要自己實作這一步。
設定 cell 的 Reuse Identifier
點選 cell,切換到 Attributes inspector 分頁,將它的 Reuse Identifier 設為 PrinceCollectionViewCell
。
連結 cell 上圖片的 outlet,宣告 reuseIdentifier
產生 cell 時必須傳入 cell 的 reuseIdentifier,我們先宣告儲存 id 的 reuseIdentifier,方便之後存取。
class PrinceCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "\(PrinceCollectionViewCell.self)"
@IBOutlet weak var imageView: UIImageView!
}
移除 PrinceCollectionViewController 裡預設產生的兩段程式
- 移除 reuseIdentifier
我們將使用在 cell 類別裡宣告的 reuseIdentifier。
private let reuseIdentifier = "Cell"
- 移除呼叫 register(_:forCellWithReuseIdentifier:) 的程式
因為我們要使用 storyboard 裡設計的 cell,因此要移除以下這行程式。(ps: 從程式或 xib 設計 cell 才會呼叫 register)
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
定義資料的型別 Prince
struct Prince {
let name: String
let image: String
}
宣告儲存資料的 array
- 寫法 1
呼叫 closure。
class PrinceCollectionViewController: UICollectionViewController {
let princes: [Prince] = {
var princes = [Prince]()
for i in 0...20 {
let prince = Prince(name: "小王子\(i)號", image: "pic\(i)")
princes.append(prince)
}
return princes
}()
- 寫法 2
呼叫 map。
class PrinceCollectionViewController: UICollectionViewController {
let princes = (0...20).map { Prince(name: "小王子\($0)號", image: "pic\($0)") }
設定 section & item 的數量
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return princes.count
}
產生 cell,設定 cell 的內容
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PrinceCollectionViewCell.reuseIdentifier, for: indexPath) as! PrinceCollectionViewCell
let prince = princes[indexPath.item]
cell.imageView.image = UIImage(named: prince.image)
return cell
}
說明
indexPath.item 告訴我們要回傳的 cell 是第幾個,item 0 表示第一個 cell,item 1 表示第二個 cell,因此我們可由 princes[indexPath.item]
取得 cell 要顯示的資料。(假設只有一個 section)
設定 cell 尺寸
我們必須告訴 collection view 每個 cell 多大,常見的設定有以下兩種。
- 設定固定大小的 cell,比方寬高 100 * 100。
- 設定一排固定數量的 cell,不同的 iPhone 尺寸將有不同大小的 cell。
設定固定大小的 cell
比方想讓 cell 固定大小 102 * 102,我們有三種方法,相關說明可參考以下連結。
設定一排固定數量的 cell,不同的 iPhone 尺寸將有不同大小的 cell
利用剛剛介紹的方法我們可以指定固定的照片大小,不過當我們設計照片牆頁面時,通常會希望一排顯示固定的照片數量,比方類似以下一排顯示 3 張照片的 IG 畫面。
想實現這樣的頁面,cell 的大小必須隨著不同的 iPhone 尺寸變化。例如想實現一排 3 張正方形的照片,照片間的間距為 4,總共有 2 個間距,那麼在寬度 428 的 iPhone 13 Pro Max 時,cell 的寬度將為
(428 - 4 * 2) / 3 = 140
因此 cell 的大小為 140 * 140。
在寬度 375 的 iPhone SE 3 時,cell 的寬度將為
(375 - 4 * 2) / 3 = 122.33
因此 cell 的大小可設為 122 * 122。
了解計算的原理後,我們可透過以下兩種方法實現:
- 方法1: 從程式計算 cell 的大小
override func viewDidLoad() {
super.viewDidLoad()
configureCellSize()
}
func configureCellSize() {
let itemSpace: Double = 4
let columnCount: Double = 3
let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
let width = floor((collectionView.bounds.width - itemSpace * (columnCount-1)) / columnCount)
flowLayout?.itemSize = CGSize(width: width, height: width)
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = itemSpace
flowLayout?.minimumLineSpacing = itemSpace
}
說明
let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
UICollectionViewController 的 property collectionViewLayout 的型別為 UICollectionViewLayout,由於我們在 storyboard 將 collection view 的 Layout 設為 Flow,因此我們可將它轉型為 UICollectionViewFlowLayout,之後再透過它的 property 設定 cell 的大小和間距。
ps: 若是另外將 collection view 加到 view controller 的 view,沒有使用 UICollectionViewController,則要從 collection view 讀取 property collectionViewLayout。
let width = floor((collectionView.bounds.width - itemSpace * (columnCount-1)) / columnCount)
利用前面提到的原理算出 cell 的寬度。值得注意的,我們利用 function floor 將小數點後的數字捨去,因為還有小數點的話,有可能會讓最後加起來的寬度超過螢幕寬度。
flowLayout?.minimumInteritemSpacing = itemSpace
flowLayout?.minimumLineSpacing = itemSpace
將 Min Spacing For Cells & Min Spacing For Lines 都設為 itemSpace。
結果:
在不同的 iPhone 都會一排顯示 3 張照片。
ps: 以上例子使用 collection view controller 顯示整個頁面,因此 collection view 的寬度將等於螢幕的寬度。若是使用 view controller + collection view,或是想在轉向時重新計算 cell 大小,還有一些小地方要調整,相關資訊可參考以下連結。
- 方法2: 讓 collection view 使用 auto layout 自動計算 cell 大小。
從 Interface Builder 為 image view 加上寬度 100 和比例 1:1 的條件。寬度 100 只是一個暫時的數字,之後我們將從程式裡修改。
ps: 為了防止 App 執行時出現 Unable to simultaneously satisfy constraints
的警告,我們可將 width constraint 的 Priority 降為 999。
連結寬度 100 條件的 outlet。
class PrinceCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "\(PrinceCollectionViewCell.self)"
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageWidthConstraint: NSLayoutConstraint!
}
collection view 的 Estimate Size 設為 Automatic,Min Spacing For Cells 設為 4,For Lines 設為 4。
當 storyboard 裡的 cell 產生時,它會先呼叫 function awakeFromNib,因此我們在裡面將 widthConstraint 設成依據 iPhone 螢幕計算的寬度。(假設一排 3 張照片,間距為 4)
class PrinceCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "\(PrinceCollectionViewCell.self)"
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageWidthConstraint: NSLayoutConstraint!
static let width = floor((UIScreen.main.bounds.width - 4 * 2) / 3)
override func awakeFromNib() {
super.awakeFromNib()
imageWidthConstraint.constant = Self.width
}
}
結果
設定 Section 上下左右間距的 section insets
我們也可以設定整個 section 區塊上下左右的 inset。如下圖所示,照片和左右邊界的距離可透過 section insets 設定。
設定 section insets 的方法有以下兩種:
- 方法1: 從 Interface Builder 設定。
點選 collection view,切換到 Size inspector 分頁,將 Section Insets 的 Left & Right 設為 10。
- 方法2: 從程式設定。
設定 UICollectionViewFlowLayout 的 sectionInset。
override func viewDidLoad() {
super.viewDidLoad()
let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
flowLayout?.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
}
設定 section insets 後,假設我們使用之前修改 image view 寬度條件的範例來修改,只要再扣除左右邊界 10 points 的間距,即可算出正確的寬度。
class PrinceCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "\(PrinceCollectionViewCell.self)"
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageWidthConstraint: NSLayoutConstraint!
static let width = floor((UIScreen.main.bounds.width - 4 * 2 - 10 * 2) / 3)
override func awakeFromNib() {
super.awakeFromNib()
imageWidthConstraint.constant = Self.width
}
}
結果
每個 cell 不同 size
我們也可以讓每個 cell 有不同的 size,透過設定 collection view 的 delegate,遵從 protocol UICollectionViewDelegateFlowLayout,然後定義 function collectionView(_:layout:sizeForItemAt:)。
extension PrinceCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item.isMultiple(of: 2){
return CGSize(width: 50, height: 50)
} else {
return CGSize(width: 200, height: 200)
}
}
}
ps: 記得 collection view 的 Estimate Size 要設為 None,才不會透過 auto layout 計算 cell 大小。
結果
cell 的大小等於 collection view 的大小
利用 UIBackgroundConfiguration 在 collection view cell 顯示圖片
範例連結
- 使用 UICollectionViewController,以 UICollectionViewFlowLayout itemSize 設定 cell 大小。
- 將 UICollectionView 加到 view controller 上,以 UICollectionViewFlowLayout itemSize 設定 cell 大小。
- 使用 UICollectionViewController,搭配 auto layout 計算 cell 大小。