利用 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 大小。

進階應用: cell 裡包含圖片跟文字

練習作業

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com