마켓컬리 검색 뷰를 클론코딩하는 도중, 전체 tableView를 만들고 tableViewCell들 안에 또 collectionView를 넣는 나를 보고....
1. 이런 비효율적인 짓을 해야 하나? 라는 생각과...
2. 애플이 나보다 바보일 리가 없어......... 라는 생각이 들었고 역시나 Compositional Layout이라는 것이 있었음 🥲
Compositional Layout을 이해하기 위해, WWDC를 먼저 보기로!
복잡한 레이아웃을 쉽게 구현하고, 기존 custom layout의 단점들을 보완하여 나온 Compositional Layout
item > group > section > layout
하나의 layout에 section, section 안에 group, group 안에 item들이 있음
예시로 코드를 살짝 본다면!
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)
이런 식으로 item, group, section, layout을 사용하여 compositional layout을 구현함
container의 너비의 50%만큼의 너비
container의 너비, 높이의 25%
높이를 200point로 지정
높이를 200으로 estimate(추정)한 경우
createLayout()
: compositional layout 생성 함수 선언💡 흥미로운 점은 다른 레이아웃을 구성하기 위해서 이 코드를 많이 변경할 필요가 없다는 점
.fractionalWidth(0.2)
코드 사용count
parameter을 사용하여 초기화함group.interItemSpacing
: group 안의 item 간격 관리 - 이 코드에서는 10포인트의 고정 간격section.interGroupSpacing
: 각 group들 사이 간격 관리 - 이 코드에서는 10포인트의 고정 간격section.ContentInsets
: 섹션의 inset 관리 - 이 코드에서는 section이 하나이므로 전체 레이아웃에 적용됨// createLayout() 함수
let spacing = CGFloat(30)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = CGFloat(10)
UICollectionViewCompositionalLayout은 provider 클로저로 섹션마다 다양한 레이아웃을 정의
라고 앞서 언급했었음
코드가 많이 달라 보이지만, 실제로는 레이아웃을 인스턴스화하는 코드일 뿐이니 겁먹지 말자!
클로저에서는,
sectionIndex
: 어떤 section인지 알려주는 indexlayoutEnvironment
: 다양한 유용한 속성들을 포함하는 파라미터SectionLayoutKind
관련 코드를 작성하였고, 이는 위에 enum으로 정의되어 있음// group의 높이를 결정할 때 사용
let groupHeight = columns == 1 ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
// horizontal group을 인스턴스화할 때 column 수를 명시적으로 전달
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
func columnCount(for width: CGFloat) -> Int {
let wideMode = width > 800
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
}
let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
columnCount
이라는 함수 선언하여, layoutEnvironment
(전체 컨테이너에 대한 정보가 포함된)에서 가져오는 너비 전달enum SectionLayoutKind: Int, CaseIterable {
case list, grid5, grid3
func columnCount(for width: CGFloat) -> Int {
let wideMode = width > 800
switch self {
case .grid3:
return wideMode ? 6 : 3
case .grid5:
return wideMode ? 10 : 5
case .list:
return wideMode ? 2 : 1
}
}
}
width
> 800이면, wideMode로 지정static let badgeElementKind = "badge-element-kind"
.
.
.
// createLayout() 함수
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: ItemBadgeSupplementaryViewController.badgeElementKind,
containerAnchor: badgeAnchor)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])
header가 고정 가능한 경우의 코드
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: PinnedSectionHeaderFooterViewController.sectionHeaderElementKind,
alignment: .top)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: PinnedSectionHeaderFooterViewController.sectionFooterElementKind,
alignment: .bottom)
sectionHeader.pinToVisibleBounds = true
section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
pinToVisibleBounds
를 false
로headerSize
에서 너비는 정확히 알고 있지만, 높이는 정확히 명시하지 않고 44point로 추정함leadingItem
trailingItem
2개로 이루어져 있는 vertical trailingGroup
leadingItem
과 trailingGroup
으로 이루어진 horizontal containterGroup
AppStore 같은 거 보면,,, Nested CollectionView 그 잡채
요 코드 하나만 추가하면 됨
뭐하는 친구인지 쫌 더 보면
none
: section 직각 스크롤 허용 ❌continuous
: 많이 쓰는 간단하고 직관적인 스크롤continuousGroupLeadingBoundary
: 직각 스크롤하여 보이는 group의 경계에서 자연스럽게 멈춤paging
: 직각으로 paging 가능groupPaging
: 한번에 한 group씩 직각으로 paginggroupPagingCentered
: 한번에 한 group씩 직각으로 paging하며 해당 그룹을 중앙에 배치얘네에다가 물론 부수적인 코드를 추가해야 하지만 베이스 코드는 요 정도인 듯하다! dataSource까지 물론 봐야 더 이해가 잘 될 듯함
WWDC 관심 있는 주제 생기면 보겠다고 했는데 어쩌다 보니 첫 번째가 요놈이 됐다 🥲
최대한 이해해 보고 싶어서 5번 넘게 본 것 같음 ,,, 영상까지는 다 이해가 됐는데 샘플 코드를 보면 아직 많이 어려운 것 같다. ㅎㅎ,,