UICollectionViewCompositionalLayout 常見排版範例

iOS 13 推出了 UICollectionViewCompositionalLayout,幫助我們更方便排版畫面,以下連結說明它的相關排版原理。

接下來我們將以推理女王克莉絲蒂的作品為例,以 UICollectionViewCompositionalLayout 實現各種 iOS App 的常見排版。

  • 格子狀照片牆(grid)
  • 水平捲動
  • 水平捲動 & 分頁
  • 水平捲動,分頁 & 顯示下一個的部分內容
  • 水平捲動,不分頁,滑動停止時剛好停在 group 的起點
  • 水平捲動,分頁 & 顯示上一個 / 下一個的部分內容
  • 整個頁面垂直捲動,頁面裡有水平捲動的區塊
  • 水平捲動的分頁,分頁的內容垂直捲動
  • 表格(table)
  • 畫面不能捲動的九宮格

格子狀照片牆(grid)

範例 1: item & group 的高度傳入 estimated,由 auto layout 自動計算高度

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
group.interItemSpacing = .fixed(space)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
return UICollectionViewCompositionalLayout(section: section)
}

image view 比例設為 0.7,priority 要設成 999,否則會有 auto layout breaking constraint 的警告訊息。

範例 2: itemSize 設為 fractionalWidth(0.5) & fractionalWidth(0.5 / 0.7),實現寬度是高度的 0.7 倍

func generateLayout() -> UICollectionViewLayout {
let space: Double = 5
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(0.5 / 0.7))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
return UICollectionViewCompositionalLayout(section: section)
}

水平捲動

方法 1: UICollectionViewCompositionalLayoutConfiguration 的 scrollDirection 設成 horizontal

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: space)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
return UICollectionViewCompositionalLayout(section: section, configuration: configuration)
}

方法 2: 設定 orthogonalScrollingBehavior

orthogonalScrollingBehavior 設為 continuous,group 的寬度固定 200,group 的高度等於 collection view 的高度。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。(因為 collection view 採用 UICollectionViewCompositionalLayout 時,預設的捲動方向是 vertical)

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: space)
section.orthogonalScrollingBehavior = .continuous
return UICollectionViewCompositionalLayout(section: section)
}

水平捲動 & 分頁

範例 1:書本之間無間距

  • 方法 1: UICollectionViewCompositionalLayoutConfiguration 的 scrollDirection 設成 horizontal, collectionView 的 isPagingEnabled 設成 true
func generateLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
return UICollectionViewCompositionalLayout(section: section, configuration: configuration)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

  • 方法 2: orthogonalScrollingBehavior 設為 paging

orthogonalScrollingBehavior 設為 paging。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return UICollectionViewCompositionalLayout(section: section)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

範例 2: 全螢幕照片,滑動時可看到間距,滑動停止時不會看到間距

orthogonalScrollingBehavior 設為 groupPaging,利用 section 的. interGroupSpacing 設定間距。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
section.orthogonalScrollingBehavior = .groupPaging
return UICollectionViewCompositionalLayout(section: section)
}

image view 的 content mode 設為 aspect fit。

範例 3: 圖文頁面

  • 方法 1: UICollectionViewCompositionalLayoutConfiguration 的 scrollDirection 設成 horizontal, collectionView 的 isPagingEnabled 設成 true
func generateLayout() -> UICollectionViewLayout {
let inset: Double = 50
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
return UICollectionViewCompositionalLayout(section: section, configuration: configuration)
}

書名 label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

  • 方法 2: orthogonalScrollingBehavior 設為 paging

orthogonalScrollingBehavior 設為 paging,利用 item 的 contentInsets 設定內容和邊界的間距。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let inset: Double = 50
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return UICollectionViewCompositionalLayout(section: section)
}

書名 label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

水平捲動,分頁 & 顯示下一個的部分內容

orthogonalScrollingBehavior 設為 groupPaging,group 的寬度設為 fractionalWidth(0.9)。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: 0)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.orthogonalScrollingBehavior = .groupPaging
return UICollectionViewCompositionalLayout(section: section)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

水平捲動,不分頁,滑動停止時剛好停在 group 的起點

orthogonalScrollingBehavior 設為 continuousGroupLeadingBoundary,group 的寬度設為 fractionalWidth(0.9)。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: 0)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return UICollectionViewCompositionalLayout(section: section)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

水平捲動,分頁 & 顯示上一個 / 下一個的部分內容

範例 1: group 裡有一個 item

使用 groupPagingCentered。從 item 的 contentInsets 設間距,item 之間的間距將是 space * 2。另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let space: Double = 5
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: space)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
return UICollectionViewCompositionalLayout(section: section)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

範例 2: group 裡有兩個 item

另外記得要將 collectionView 的 isScrollEnabled 設為 false,不然畫面還是可以垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let space: Double = 5
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: space)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 2)
group.interItemSpacing = .fixed(space * 2)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
return UICollectionViewCompositionalLayout(section: section)
}

label 的 vertical content hugging 設為 252,vertical content compression 設為 751。

整個頁面垂直捲動,頁面裡有水平捲動的區塊

範例 1: 多個 section,每個 section 搭配一樣的 NSCollectionLayoutSection 排版

func generateLayout() -> UICollectionViewLayout {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200), heightDimension: .absolute(318))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: space, bottom: 0, trailing: space)
section.orthogonalScrollingBehavior = .continuous
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return UICollectionViewCompositionalLayout(section: section)
}

範例 2: 多個 section,每個 section 搭配不同的 NSCollectionLayoutSection 排版

分成多個 section,依據以下規則排版:

  • section number 偶數的 section

group 延水平方向排列,可水平滑動。

  • section number 奇數的 section

group 延垂直方向排列。

func generateLayout() -> UICollectionViewLayout {

UICollectionViewCompositionalLayout { [unowned self] section, environment in
if section.isMultiple(of: 2) {
return self.horizontalScrollLayoutSection
} else {
return self.verticalScrollLayoutSection
}
}
}


var horizontalScrollLayoutSection: NSCollectionLayoutSection {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(200), heightDimension: .absolute(318))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
section.orthogonalScrollingBehavior = .continuous
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}

var verticalScrollLayoutSection: NSCollectionLayoutSection {
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(143))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}

設定搭配不同 section 的 cell,section number 奇數的 section 顯示 VerticalCollectionViewCell,section number 偶數的 section 顯示 HorizontalCollectionViewCell。VerticalCollectionViewCell 的圖片比例 0.7,HorizontalCollectionViewCell 的圖片寬度 100。

水平捲動的分頁,分頁的內容垂直捲動

將 configuration.scrollDirection 設成 horizontal,collectionView 的 isPagingEnabled 設成 true,實現水平滑動的分頁,section.orthogonalScrollingBehavior 設成 continuous 讓分頁的內容垂直捲動。

func generateLayout() -> UICollectionViewLayout {
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
return UICollectionViewCompositionalLayout(sectionProvider: { section, environment in
let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: section == 0 ? 2 : 3)
group.interItemSpacing = .fixed(space)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
section.orthogonalScrollingBehavior = .continuous
return section

}, configuration: configuration)
}

image view 比例設為 0.7,priority 要設成 999,否則會有 auto layout breaking constraint 的警告訊息。

表格(table)

範例 1: iOS 14 的 UICollectionViewCompositionalLayout.list

使用 UICollectionViewCompositionalLayout.list 產生以表格樣式排版的 UICollectionViewCompositionalLayout,cell 高度將以 auto layout 條件自動計算。

func generateLayout() -> UICollectionViewLayout {

let configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
return UICollectionViewCompositionalLayout.list(using: configuration)

}

範例 2:

item 的大小等於 group 的大小,group 的寬度等於 collection view 的寬度。

func generateLayout() -> UICollectionViewLayout {

let space: Double = 10
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(143))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = space
return UICollectionViewCompositionalLayout(section: section)
}

畫面不能捲動的九宮格

除了設定 layout,記得也要將 collection view 的 isScrollEnabled 設為 false。

func generateLayout() -> UICollectionViewLayout {
let space: Double = 3
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: space, leading: space, bottom: space, trailing: space)
return UICollectionViewCompositionalLayout(section: section)
}

--

--

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

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

彼得潘和學生們在開發 Swift iOS App 路上曾經解決的問題集

彼得潘的 iOS App Neverland
彼得潘的 iOS App Neverland

Written by 彼得潘的 iOS App Neverland

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