Compositional Layout
- Composable: 복잡한 것을 단순한 것들로 구성할 수 있는
- Flexible: 어떠한 레이아웃도 작성할 수 있을 정도로 유연한
- Fast: 빠른
Composing small layout groups together
Layout groups are line-based
Composition instead of subclassing (UICollectionViewFlowLayout)

- subclassing
Item, Group, Section, Layout
let size = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(44)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: size,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
4가지 중요 키워드
- item
- group
- section
- layout

- 화면 전체에 해당하는 것이 layout
- layout은 section으로 구성되어있다. 기존의 것과 마찬가지.
- section은 group으로 구성되어있다. group은 row와 같다.
- group은 item으로 구성되어있다.
Core Concepts

1. NSCollectionLayoutSize

- width와 height이 있지만 단순한 scalar값들이 아니라
NSCollectionLayoutDimension
타입을 가지고 있다.
NSCollectionLayoutDimension ?? 🤔

- 특정 축이 얼마나 큰지를 설명하는 축 독립적인 방식입니다.
fractionalWidth, fractionalHeight

- container(CollectionView)의 width의 0.5

width와 height를 같이 조합하면..

absolute
- 이건 그냥 말그대로 절대값. 고정값을 갖게 해준다.
estimated

- 얼마나 크게 될지 잘은 모르겠다…? 아마 200 정도??
- 그리고 시간이 지남에 따라 item이 렌더링되고 해당 item의 콘텐츠에 대해 조금씩 더 많이 알게 되면 그 수가 증가한다.

2. NSCollectionLayoutItem✨

이것보다 조금 더 있다. 공식문서 링크 첨부
- cell 아니면 supplementary.
- 화면에 렌더링하는 것들.
- Size를 받는다.
3. NSCollectionLayoutGroup✨

Group이 핵심이다.
- 함께 구성할 레이아웃의 기본단위
Group의 3가지 형태
- horizontal
- vertical
- custom : 미니 flow layout이라고 생각하면 된다.
4. NSCollectionLayoutSection✨

- CollectionView의 section별 레이아웃 정의.
- 해당 섹션에 몇 개의 items이 있는지에 대한 dataSource 개념에 직접 매핑된다.
- Group을 받는다.
5. UICollectionViewCompositionalLayout✨

https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout
여기서 흥미로운 점은 플랫폼에 관계없이 이 모든 항목에 대한 정의가 동일하다는 점이다.
Code Examples
List

private func createLayout() -> UICollectionViewCompositionalLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(44)
)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
- 여기서 주목해야할 점은 이 경우에서 궁극적으로 item size를 결정짓는 것은 group size이다. (container-relevant sizing)
- 이 상황에서는 각각의 item이 각각의 group을 가지게 된다.
- group은 row하나에 해당하게 된다. (늘 그런거 아님. 해당 예제에서 그렇다는 뜻.)
- group의 사이즈를 우리는 CollectionView의 width에 44의 height를 갖게 한 다음, item size를 width와 height을 모두 100%로 해서 item의 container가 group이다 라고 정해주면 위의 사진과 같이 된다.
Grid

private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2)
)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
- 위의 예제와 다른점은 item과 group 사이즈뿐이다.
- group size
- width = fractionalWidth(1) → 너비는 collectionview(conatiner)의 width만큼.
- height = fractionalWidth(0.2) → 한 row에 5개가 들어가기 위해선 width는 0.2만큼의 비율을 가지고 있어야함. 해당 너비를 높이를 주어 정사각형을 만든다.
- item Size
- fractionalWidth(0.2) → 한 row에 5개가 들어가기 위해선 width는 0.2만큼의 비율을 가지고 있어야함
- fractionalHeight(1) → group에서 높이를 지정해놨기 때문에 거기에 맞추면 된다.
이제 여기에 items들 사이에 여백을 두고자하면 어떻게 할까?
add inset between items (1) - item contentInsets

func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(
top: 5,
leading: 5,
bottom: 5,
trailing: 5
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2)
)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
- item.contentInsets에 inset값 부여
- 이미 계산된 레이아웃에서 마지막 단계로 빼는 것과 비슷하다.
Two-Column Grid

func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44)
)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitem: item,
count: 2
)
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 10,
bottom: 0,
trailing: 10
)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
- row를 대표하는 group을 여기서는 조금 다르게 구현하였다.
count가 포함된 생성자를 이용해서 width값 설정
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitem: item,
count: 2
)
- count가 포함된 생성자를 사용한다.
- count만큼의 subitem이 group당 있었으면 좋겠어 !
- 즉, 하나의 row당 2개의 items들이 있었으면 좋겠어!!
- 이제 이렇게 하면 컴포지션 레이아웃이 자동으로 item width가 얼마인지 파악하여 이를 구현한다.
오잉? 전 위에 item width를 .fractionalWidth(1.0)로 정의해줬는데요..??🤔
만약 우리가 그룹당 item개수를 알려준다면 사실상 이 값들은 모두 컴포지셔널 레이아웃에 의해 overriden(재정의) 될 것이다.
add inset between items (2)
section contentInsets
앞에서는 item.contentInset으로 여백을 줬었는데 이번에는 다른 방법이다.
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 10,
bottom: 0,
trailing: 10
)
- section의 contentInset에 값을 부여하기
- 해당 예제에서는 섹션이 하나밖에 없기 때문에 이것은 모든 layout에 적용됨.
interItemSpacing, interGroupSpacing
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing) // group안에서 item간의 간격 주기
section.interGroupSpacing = spacing // section에서 group간 간격 주기
// 해당 예제에서 group이 하나의 row이기 때문에 row간의 간격을 주는 효과를 가짐.ㅇ
section마다 다른 layout 주기✨
multiple sections having their own distinct layout !

func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = sectionLayoutKind.columnCount
// The group auto-calculates the actual item width to make
// the requested number of columns fit, so this widthDimension is ignored.
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 2,
trailing: 2
)
let groupHeight = columns == 1
? NSCollectionLayoutDimension.absolute(44)
: NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight
)
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitem: item,
count: columns
)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: 20,
leading: 20,
bottom: 20,
trailing: 20
)
return section
}
return layout
}
- 언뜻 보기에는 크게 달라 보이지만 실제로는 레이아웃을 인스턴스화하는 가장 바깥쪽 컴포넌트가 마지막에 오는 것이 아니라 처음에 온다는 점만 다르다.

sectionProvider
클로저를 작성하는 것이다.
UICollectionViewCompositionalLayout { <#Int#>, <#NSCollectionLayoutEnvironment#> in
<#code#>
}
- sectionIndex: Int → 몇번째 섹션인지
- layoutEnvironment: NSCollectionLayoutEnvironment
- 이제 컴포지션 레이아웃은 특정 섹션에 대한 새로운 description을 요청해야 할 때마다 이 클로저를 자동으로 호출한다.

- escaping closure로 되어있는 모습
다시 코드로 돌아와서...
enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
var columnCount: Int {
switch self {
case .grid3:
return 3
case .grid5:
return 5
case .list:
return 1
}
}
}
guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = sectionLayoutKind.columnCount
- 해당 섹션의 column count를 가져온다.
let group = NSCollectionLayoutGroup
.horizontal(
layoutSize: groupSize,
subitem: item,
count: columns
)
- count에 해당 열의 개수를 넘기면
- 컴포지셔널 레이아웃이 자동으로 item의 width를 계산해낸다.
Adaptive rotating layout

- 이전 예시

- Adaptive Section예시
enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
func columnCount(for width: CGFloat) -> Int {
let wideMode = width > 700
switch self {
case .grid3:
return wideMode ? 6 : 3
case .grid5:
return wideMode ? 10 : 5
case .list:
return wideMode ? 2 : 1
}
}
}
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let layoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = layoutKind == .list
? NSCollectionLayoutDimension.absolute(44)
: NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
- 다른 점은 columCount가 더이상 get-only property가 아니고 함수로 바뀐점이다.
- 해당 함수에 인자로 넘기는 값을 살펴볼 필요가 있다.
- layoutEnvironment에서 가져온 width를 전달하고 있다. 따라서 레이아웃 환경 유형에는 레이아웃이 작동해야 하는 전체 컨테이너 너비와 같은 정보가 포함되며, iOS에서는 특성 컬렉션(Trait collection) 정보도 내부에 있다. 따라서 이 모든 정보를 사용하여 현재 환경에 어떤 종류의 레이아웃이 적합한지 파악할 수 있다. 여기서는 너비만 사용한다.
- 가로 및 세로 크기 클래스, 디스플레이 배율, 사용자 인터페이스 관용구 등의 특성을 포함한 앱의 iOS 인터페이스 환경입니다.
'Dev > WWDC 정리' 카테고리의 다른 글
[WWDC22] Design protocol interfaces in Swift (1) | 2023.04.27 |
---|---|
[WWDC19] Advances in Collection View Layout (Compositional Layout 정리) - 2(完) (1) | 2023.04.27 |
[WWDC18] iOS memory deep dive - 1 (1) | 2023.03.27 |
[WWDC21] Use async/await with URLSession + 적용 (0) | 2023.03.23 |
[WWDC16] Understanding Swift Performance 2부 (1) | 2023.03.03 |
댓글