나오게 된 이유
iOS6에서 출시된 UICollectionViewLayout을 통한 추상화로 인해 LineBasedLayout으로 레이아웃을 쉽게 할 수 있었으나, 시간이 지나면서 점점 복잡해짐. 따라서 기존 lineBasedLayout을 사용하지 않고 사용자 정의 레이아웃을 구축해야 하는데, 이를 구현하는 데는 까다로워 이를 위해 나온 레이아웃이 Compositional Layout임.
CompositionalLayout 기본 구성
Layout: 콜렉션 뷰의 레이아웃
Section: 각 섹션
Group: 각 섹션의 행. 앞으로 반복하게 될 반복 구조, 항목의 열 또는 행을 나타냄.
item: 콜렉션 뷰의 셀
크기 정의
각 요소의 크기를 지정하기 위해 Compositional Layout은 NSCollectionLayoutDimension이라는 타입을 이용해 지정한다.
NSCollectionLayoutDimension
특정 축의 크기를 정의하는 방식. 총 4가지 방식이 있다.
- .fractionalWidth/Height: 콜렉션 뷰 크기의 종횡비로 크기를 지정
- .absolute: 상수를 이용하여 크기를 지정. 고정된 크기
- .estimated: 예측한 크기
NSCollectionLayoutItem
아이템의 크기를 정의하는 레이아웃
NSCollectionLayoutGroup
그룹의 사이즈와 레이아웃을 지정. horizontal, vertical을 이용해서 line-based처럼 정의하거나, Custom 하게 레이아웃을 잡을 수 있다.
NSCollectionLayoutSection
섹션별 레이아웃 정의.
그리드 레이아웃
이렇게 2개의 열을 가진 레이아웃을 잡으려고 했을 때 2가지 방식으로 구현 할 수 있다.
func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
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,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
첫 번째 방식으로 item의 width를 0.5로 두어 group에 2개의 셀이 들어갈 수 있게 크기를 잡는 방법이 있고,
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)
이런 식으로 group를 생성할 때 count를 이용해 그룹 내부에 들어갈 아이템의 개수를 정의한다. 이때 아이템의 사이즈를 0.5로 설정한다고 하더라도, group을 통해 재정의 한 것으로 쳐서 아이템의 사이즈는 그룹의 개수에 맞게 계산된다.
두 방식의 차이점
첫번째 방식으로 구현했을 경우 inset을 주려고 하면 item의 사이즈가 group의 0.5로 설정되어 있어 다음과 같이 화면이 출력된다.
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
이유는 width가 group을 기준으로 잡혀있기에 아이템을 하나의 그룹에 inset을 포함해 2개를 넣을 수 없기 때문이다.
하지만 두 번째 방식의 경우에는
이런 식으로 2개의 아이템이 하나의 그룹 내부에 들어간 것을 볼 수 있다. 앞서 말했듯이 그룹에 의해 재정의 됨으로써 아이템의 사이즈가 다시 계산되었기 때문이다.
섹션별 다른 레이아웃
이런 식으로 섹션별로 다른 레이아웃을 지정하고 싶을 때 UICollectionViewCompositionalLayoutSectionProvider 클로저를 이용하면 각 섹션별로 레이아웃을 을때 해당 클로저가 호출되면서 인자값으로 섹션 index 값이 들어오므로 더 쉽게 레이아웃을 잡을 수 있다.
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
}
}
}
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
let columns = sectionLayoutKind.columnCount
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
}
Decoration Item
콜렉션 뷰 셀에 있는 배지 같은 셀에 표시를 할 때 사용한다.
사용방식은 decoration의 위치를 설정하고, 사이즈와 decoration을 구분할 고유한 식별자를 넣어주면 된다. 고유한 식별자는 공식문서에서는 이런 식으로 쉽게 추적할 수 있게 하는 것을 추천하고 있다.
struct ElementKind {
static let badge = "badge-element-kind"
static let background = "background-element-kind"
static let sectionHeader = "section-header-element-kind"
static let sectionFooter = "section-footer-element-kind"
static let layoutHeader = "layout-header-element-kind"
static let layoutFooter = "layout-footer-element-kind"
}
let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top], fractionalOffset: CGPoint(x: 0.3, y: -0.3))
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),
heightDimension: .absolute(20))
let badge = NSCollectionLayoutSupplementaryItem(
layoutSize: badgeSize,
elementKind: ItemBadgeSupplementaryViewController.badgeElementKind,
containerAnchor: badgeAnchor)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])
section header, footer
header와 footer 역시 사용법이 비슷하다.
let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: SectionHeadersFootersViewController.sectionHeaderElementKind, alignment: .top)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: SectionHeadersFootersViewController.sectionFooterElementKind, alignment: .bottom)
section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
섹션별 decorationView
각 섹션마다 커스텀한 뷰를 백그라운드로 표시 할 수 있다. 사용법은 각 뷰의 식별자를 이용하여 생성한 후, layout에 register 한다.
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 5
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(
elementKind: SectionDecorationViewController.sectionBackgroundDecorationElementKind)
sectionBackgroundDecoration.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
section.decorationItems = [sectionBackgroundDecoration]
let layout = UICollectionViewCompositionalLayout(section: section)
layout.register(
SectionBackgroundDecorationView.self,
forDecorationViewOfKind: SectionDecorationViewController.sectionBackgroundDecorationElementKind)
그룹 중첩
Compositional layout의 핵심은 group. group은 중첩이 가능하며 이를 이용해 복잡한 레이아웃을 만들 수 있다.
이런 식으로 그룹을 만들어서 파란색 그룹에 붉은색 그룹과 item을 추가한다.
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
heightDimension: .fractionalHeight(1.0)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)))
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)
let nestedGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
return section
}
콜렉션뷰 중첩
콜렉션 뷰 내부의 섹션을 다른 방향으로 스크롤 하려면, 각 섹션이나 셀에 콜렉션 뷰를 추가하여 각 콜렉션 뷰마다의 코드가 필요했지만, Compositional Layout을 사용하면 이를 최상위 뷰 하나로 병합 시킬 수 있음.
코드 역시 간단하게 각 섹션에 적용할 behavior을 적용해주기만 하면 됨.
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection?
.
.
.
.
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85),
heightDimension: .fractionalHeight(0.4)),
subitems: [leadingItem, trailingGroup])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = .continuous //적용
return section
}
return layout
}
NSCollectionLayoutSectionOrthogonalScrollingBehavior
각 섹션을 어떤 식으로 스크롤을 할지 결정할 enum. 스크롤은 해당 CollectionVIew의 layout의 scrollDirection의 수직이 되게 자동으로 적용이 된다. 콜렉션 뷰가 vertical 하게 스크롤 하도록 되어 있다면, section은 자동으로 horizontal하게 스크롤하도록 된다. 즉 콜렉션뷰가 vertical하게 스크롤 되도록 하고, section도 vertical하게 스크롤하지 못한다.
참고: https://developer.apple.com/videos/play/wwdc2019/215, https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views
'iOS > UIKit' 카테고리의 다른 글
NSCollectionViewDiffableDataSource를 apply를 했음에도 Cell의 내용이 바뀌지 않는다면 (1) | 2023.11.01 |
---|---|
DiffableDatasource (0) | 2023.09.19 |
UIScrollView의 작동 방식과 Frame Layout Guide, Content Layout Guide, contentOffset (0) | 2023.08.13 |
UIView와 CALayer (0) | 2023.07.26 |
CAShapeLayer, CABasicAnimation을 이용하여 버튼 누를 시 원이 그려지는 애니메이션 + 애니메이션이 완료되었을 때 원하는 작업 수행하기 (0) | 2023.06.01 |