앱스토어 앱.
CompositionalLayout을 사용하지 않는다면
테이블뷰 내부의 셀 내부의 컬렉션뷰 내부의 테이블뷰...
이런 복잡한 레이아웃을 보완하기위해 Compositional Layout의 등장.
전체 하나의 CollectionView로 섹션별 다양한 레이아웃을 적용 가능
구성
하나의 Layout 내부의 여러 Section이 포함되며
Section은 여러 Group을 포함한다.
Group은 Item을 갖고있다.
이들의 크기를 지정해주면 된다.
CollectionView의 item 사이즈를 정하는 방법(NSCollectionLayoutDimension)
- .absolute - 고정 크기
- .estimated - 런타임에 크기가 정해지는경우(예상크기)
- .fractional - 비율(컨테이너에 대한 비율)
단일 섹션 예시
item > group > Section 순서로 정의
private func makeCompositionalLayout() -> UICollectionViewCompositionalLayout {
// Item 정의: 각 아이템의 크기를 정의합니다. 여기서는 아이템의 너비를 그룹의 1/3로,
// 높이를 그룹의 높이와 동일하게 설정하여, 한 줄에 3개의 아이템이 들어가는 그리드를 형성합니다.
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3), // group 내에서 아이템의 너비를 그룹 너비의 1/3로 설정
heightDimension: .fractionalHeight(1) // 아이템의 높이를 그룹의 높이와 동일하게 설정
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// 아이템 간 간격 설정: 각 아이템 주변에 상, 하, 좌, 우로 5포인트의 여백을 추가합니다.
item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
// Group 정의: 그룹의 크기를 정의합니다. 여기서는 그룹의 너비를 컬렉션 뷰의 전체 너비와 동일하게,
// 높이를 컬렉션 뷰의 높이의 1/4로 설정하여, 세로 방향으로 최대 4개의 그룹이 들어갈 수 있게 합니다.
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1), // 컬렉션 뷰에 대해 그룹의 너비를 전체 너비로 설정
heightDimension: .fractionalHeight(1/4) // 컬렉션 뷰 높이의 1/4로 그룹의 높이를 설정
)
// 수평 그룹 생성: 위에서 정의한 아이템을 포함하는 수평 방향 그룹을 생성합니다.
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// Section 정의: 위에서 정의한 그룹을 포함하는 섹션을 생성합니다.
// 이 섹션 설정은 컬렉션 뷰 전체에서 하나의 섹션을 정의합니다.
let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)
}
다중섹션 다른 레이아웃 적용 예시
다중섹션을 표현하기 위한 데이터는 다음과 같이 준비하였습니다.
enum Section {
case first([DataSource])
case second([DataSource])
case third([DataSource])
}
private let dataSource: [Section] = [
.first((1...6).map { DataSource(index: $0, backbroundColor: .randomColor)}),
.second((1...2).map { DataSource(index: $0, backbroundColor: .randomColor)}),
.third((1...5).map { DataSource(index: $0, backbroundColor: .randomColor)})
]
섹션이란 Enum의 각 case가 Section이 될것이고 연관값으로 가지고있는 [DataSource]가 섹션 내부에 표시될 데이터입니다.
그럼 컬렉션뷰의 섹션을 표시할 때 DataSource메서드들은 어떻게 표시되어야 할까요
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// section으로는 Int값이 들어옵니다 dataSource는 [Section] 배열입니다.
// 각 case들은 연관값으로 데이터를 갖고있습니다. 섹션별로 해당 아이템의 개수를 리턴해줍니다.
switch self.dataSource[section] {
case let .first(items):
return items.count
case let .second(items):
return items.count
case let .third(items):
return items.count
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
// 섹션의 개수를 나타냅니다
return self.dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.defaultReuseIdentifier, for: indexPath) as? Cell else { return UICollectionViewCell() }
switch self.dataSource[indexPath.section] {
case let .first(items):
cell.initSetting(data: items[indexPath.item])
case let .second(items):
cell.initSetting(data: items[indexPath.item])
case let .third(items):
cell.initSetting(data: items[indexPath.item])
}
return cell
}
}
위 코드에서 전 처음보는것이 있었습니다.
바로 IndexPath.item이었습니다. 해당 값이 뭘 나타내는지 알아봅시다.
IndexPath.row는 UITableView에서 주로 사용됩니다. UITableView는 섹션과 행(row)의 개념을 가지고 있으며, 리스트 형태의 데이터를 세로로 나열할 때 사용됩니다. 따라서, UITableView에서는 특정 섹션 내의 행을 식별하기 위해 indexPath.row를 사용합니다.
indexPath.item은 UICollectionView에서 사용됩니다. UICollectionView는 더 유연한 레이아웃을 제공하며, 그리드 형태의 데이터 표시에 주로 사용됩니다. UICollectionView에서는 각 셀을 '아이템(item)'으로 취급하며, 특정 섹션 내의 아이템을 식별하기 위해 indexPath.item을 사용합니다.
결론적으로, indexPath.row와 indexPath.item은 같은 인덱스를 참조하지만, UITableView와 UICollectionView의 문맥에서 각각 적절히 사용되는 용어입니다. 코드의 가독성과 일관성을 위해 각 컨텍스트에 맞는 용어를 사용하는 것이 좋습니다.
기능은 같지만 컬렉션뷰와 테이블뷰의 특성에 따라 다른 용어를 사용한다는 의미네요!
그렇다면 이제 이 데이터들을 가지고 Compositional Layout을 어떻게 사용하는지 알아봅시다!
private func makeCompositionalLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { section, environment in
switch section {
case 0:
let itemWidth = 1.0/3.0
let groupHeight = 1.0/4.0
let itemInset: CGFloat = 2.5
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(itemWidth),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(groupHeight)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
return section
case 1:
let itemWidth = 1.0/2.0
let groupHeight = 1.0/5.0
let itemInset: CGFloat = 0
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(itemWidth),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(groupHeight)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
return section
default:
let itemWidth = 1.0
let groupHeight = 1.0/4.0
let itemInset: CGFloat = 3
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(itemWidth),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: itemInset, leading: itemInset, bottom: itemInset, trailing: itemInset)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(groupHeight)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
return section
}
}
}
오우 길죠
첫 번째 섹션은
하나의 셀이 그룹의 크기의 가로로 1/3등분 합니다
또한 세로로 자신이 포함된 그룹을 1만큼 차지합니다.
1개의 그룹은 컬렉션뷰 전체(1)만큼 가로로 차지하고
높이는 컬렉션뷰를 1/4등분 한 것 만큼 차지합니다.
여기서 헷갈릴 수 있는 문제는 하나의 그룹이 이만큼을 차지한다는거지 그룹 전체가 컬렉션뷰 전체의 1/4만큼 차지한다는 의미가 아닙니다.
이후는 코드를 따라 읽어 내려가시다보면 아실 수있습니다.
이를 통해 만들어진 레이아웃은 다음과 같습니다.
이해를 돕기위해 컬렉션뷰를 살짝 아래로 스크롤 한 다음의 상태입니다.
깃헙
https://github.com/HyeonjunKKang/iOS---UICollectionView-Compositional-Layout
다음엔 DiffableDataSource도 한번 다뤄보겠습니다.