# Collection View Compositional Layout
iOS 6 引入了 UICollectionView (opens new window),那个时候 collection view 的布局比较简单,基本上都是基于行列的(line-based),称为 flow 布局 UICollectionViewFlowLayout (opens new window),这能满足大部分的应用场景,但是随着布局越来越复杂(例如 iOS 13 更新之后的 App Store),这个时候该怎么办呢?
一个解决方案是自定义布局(custom layout),但是自定义布局要面临几个的挑战
- Boilerplate code
- Performance considerations
- Supplementary and decoration view challenges
- Self-sizing challenges
因此 iOS 13 提出了新的解决方案 Compositional Layout: UICollectionViewCompositionalLayout (opens new window)
macOS 对应的是 NSCollectionViewCompositionalLayout (opens new window)
# Compositional Layout
Compositional Layout 是一种组合的(composable)、灵活性的(flexible)、快速的(fast)布局方式,将开发者从自定义布局中释放出来
组合布局顾名思义,是将很多个小的布局组合在一起,形成一个完整的大布局,用组合的方式代替自定义布局,由以下四个部分组成:Item -> Group -> Section -> Layout
# Demo—创建 Grid Layout
func createGridLayout() -> UICollectionViewLayout {
// Item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// Group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
// Section
let section = NSCollectionLayoutSection(group: group)
// Layout
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# New APIs
# NSCollectionLayoutDimension (opens new window) & NSCollectionLayoutSize (opens new window)
定义 collection view item 的 width 和 height,有三种方式
# absolute
固定值,以 point 为单位
let absoluteSize = NSCollectionLayoutSize(widthDimension: .absolute(44),
heightDimension: .absolute(44))
2
# estimated
预估值,用于内容大小可能会发生变化的,比如字体大小发生变化,可以用于 Self-Sizing?
let estimatedSize = NSCollectionLayoutSize(widthDimension: .estimated(200),
heightDimension: .estimated(100))
2
# fractional
定义相对于容器的比例,有 fractionalWidth
和 fractionalHeight
两种
let fractionalSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(0.2))
2
# NSCollectionLayoutItem (opens new window)
定义单个 collection view item 的大小
class NSCollectionLayoutItem {
convenience init(layoutSize: NSCollectionLayoutSize)
}
2
3
# contentInsets
定义 item 的 inset
# edgeSpacing
定义 item 与item, item与容器的间距
# supplementaryItems
Item 的附属 items
# NSCollectionLayoutGroup (opens new window)
Group 由一个或者多个 item 组成,有三种布局方式水平、垂直、自定义,通过 NSCollectionLayoutSize
确定这个 group 的大小
class NSCollectionLayoutGroup: NSCollectionLayoutItem {
class func horizontal(layoutSize: NSCollectionLayoutSize,
subitems: [NSCollectionLayoutItem]) -> Self
class func vertical(layoutSize: NSCollectionLayoutSize,
subitems: [NSCollectionLayoutItem]) -> Self
class func custom(layoutSize: NSCollectionLayoutSize,
itemProvider: NSCollectionLayoutGroupCustomItemProvider) -> Self
}
2
3
4
5
6
7
8
NSCollectionLayoutGroup
继承自 NSCollectionLayoutItem
,因此拥有 NSCollectionLayoutItem
所有的属性(layoutSize
、contentInsets
、edgeSpacing
、supplementaryItems
),同时支持 group 嵌套
interItemSpacing
确定同一个 group 中 item 之间的间距,支持两种方式
- fixed
- flexible
# NSCollectionLayoutSection (opens new window)
Section 由一个或者多个 group (因为 group 可以嵌套)组成
class NSCollectionLayoutSection {
convenience init(layoutGroup: NSCollectionLayoutGroup)
}
2
3
# orthogonalScrollingBehavior
定义 section 相对于 collection view 的滚动行为
- none,不允许正交滚动
- continuous
- continuousGroupLeadingBoudary,滚动结束时,在 group 的 leading 边缘停下
- paging
- groupPaging
- groupPagingCenter
# interGroupSpacing
定义 group 的间距
# contentInsets
定义 section 的 inset
# boundarySupplementaryItems
section 的附属 items
# decorationItems
section 的装饰 items
# UICollectionViewCompositionalLayout (opens new window)
Layout 由一个或者多个 section 组成,每个 section 的布局可以不一样
class UICollectionViewCompositionalLayout: UICollectionViewLayout {
// 单个 section
init(section: NSCollectionLayoutSection,
configuration: UICollectionViewCompositionalLayoutConfiguration)
// 多个 sections
init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider,
configuration: UICollectionViewCompositionalLayoutConfiguration)
}
2
3
4
5
6
7
8
# UICollectionViewCompositionalLayoutConfiguration (opens new window)
配置 Compositional Layout
# scrollDirection
定义 滚动方向
- vertical
- horizontal
# interSectionSpacing
定义 section 的间距
# boundarySupplementaryItems
layout 的附属 items
# Advanced Layouts
从前面的 API 中发现,item、group、section、layout 都能定义自己的附属 items,附属 items 可以是
- Badges
- Headers
- Footers
section 还可以定义装饰 items
# NSCollectionLayoutSupplementaryItem (opens new window)
这类附属 item,用于 item 和 group,通过 NSCollectionLayoutSize
确定其大小
通过 NSCollectionLayoutAnchor
确定其位置
- itemAnchor,相对于 item 本身的位置
- containerAnchor,相对于容器的位置
class NSCollectionLayoutSupplementaryItem : NSCollectionLayoutItem {
init(layoutSize: NSCollectionLayoutSize, elementKind: String, containerAnchor: NSCollectionLayoutAnchor)
init(layoutSize: NSCollectionLayoutSize, elementKind: String, containerAnchor: NSCollectionLayoutAnchor, itemAnchor: NSCollectionLayoutAnchor)
}
2
3
4
# 例如设置 item 的 badge
let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing],
fractionalOffset: CGPoint(x: 0.3, y: -0.3))
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),
heightDimension: .absolute(20))
let badge = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize,
elementKind: "badge",
containerAnchor: badgeAnchor)
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])
2
3
4
5
6
7
8
# NSCollectionLayoutBoundarySupplementaryItem (opens new window)
这类附属 item,用于 section 和 layout
class NSCollectionLayoutBoundarySupplementaryItem : NSCollectionLayoutSupplementaryItem {
init(layoutSize: NSCollectionLayoutSize, elementKind: String, alignment: NSRectAlignment)
init(layoutSize: NSCollectionLayoutSize, elementKind: String, alignment: NSRectAlignment, absoluteOffset: CGPoint)
}
2
3
4
# pinToVisibleBounds
附属 item 是否跟着 collection view 一起滚动(false,默认值)还是一直可见
# 例如设置 section 的 header 和 footer
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
elementKind: "header",
alignment: .top)
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize,
elementKind: "footer",
alignment: .bottom)
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header, footer]
2
3
4
5
6
7
8
# NSCollectionLayoutDecorationItem (opens new window)
装饰 items 用于 section,目前只有一个方法就是设置 section 的背景
class NSCollectionLayoutDecorationItem : NSCollectionLayoutItem {
class func background(elementKind: String) -> Self
}
2
3
# 例如设置 section 的背景图
let sectionBackground = NSCollectionLayoutDecorationItem.background(
elementKind: "background")
section.decorationItems = [sectionBackground]
// 注册 decoration view
let layout = UICollectionViewCompositionalLayout(section: section)
layout.register(
SectionBackgroundDecorationView.self,
forDecorationViewOfKind: "background"
)
2
3
4
5
6
7
8
9
10
# Nested Group
话不多说上代码
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
heightDimension: .fractionalHeight(1.0))
let leadingItem = NSCollectionLayoutItem(layoutSize: leadingSize)
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3))
let trailingItem = NSCollectionLayoutItem(layoutSize: trailingSize)
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
heightDimension: .fractionalHeight(1.0)),
subitem: trailingItem, count: 2)
// nested group
let nestedGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
return section
}
return layout
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Reference
- Sample Code
Implementing Modern Collection Views (opens new window)
- WWDC 2019 Session 215