처음 colllectionView를 사용했을 땐 StroyBoard에 UICollectionView DataSource, Delegate, 추상 클래스인 UICollectionViewLayout를 상속받은 UICollectionViewFlowLayout을 이용해서 크기 지정과 데이터를 관리하는 코드만 작성하였습니다.
이번에는 UICollectionViewCompositionalLayout을 사용해서 멋진 레이아웃을 만드는 법을 알아보겠습니다.
UICollectionViewCompositionalLayout
WWDC 19 에 발표되었으며, IOS13부터 사용할 수 있고 UICollectionViewLayout을 상속받아 만들어진 것입니다.
레이아웃 하나를 만들기 위해서는 아래 코드가 필수적으로 필요합니다.
func createBasicListLayout() -> 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,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
CompositionalLayout을 구성하려면..
- NSCollectionLayoutSection을 만들어줘야 합니다. 이걸 만들려면..
- NSCollectionLayoutGroup을 만들어줘야 합니다. 이걸 만들려면 GroupSize를 위한 LayouotSize와 같이..
- NSCollectionLayoutItem을 만들어줘야 합니다. Group과 마찬가지로 size를 지정합니다.
만들어진 Layout을 사용하려면 collectionViewLayout에 넣어주면 레이아웃은 완성됩니다.
collectionView.collectionViewLayout = layout()
Layout을 만드는 코드를 다시 보면 NSCollectionLayoutSize에 .fractionalWidth, .estimated, .absolute 가 있습니다.
이 메서드들의 동작 방식을 알아보겠습니다.
- .fractionWidth(), .fractionHeight()
- 감싸져 있는 컨테이너(group 또는 section)의 비율로 계산을 하여 적용됩니다.
- .estimated()
- 함수명처럼 입력값의 추정 값을 계산하여 적용됩니다.
- .absolute()
- 함수명처럼 입력한 값 그대로 절댓값으로 적용됩니다.
다음으로 알아볼 것은 NSCollectionLayoutGroup입니다.
생성에는 크게 3가지 방법이 있습니다.
Vertical은 그룹 안 아이템을 세로로 배열한다. Horizontal은 그룹 안 아이템을 가로로 배열한다. 의 차이점 이 있습니다.
custom은 해당 항목에 대한 사용자 지정 배열을 만드는 항목 공급자와 함께 지정된 크기의 그룹을 만듭니다.라고 설명돼있긴 하는데..
CustomItemProvider도 있고 해서 다음에 직접 해보고 세 번째 정리를 해봐야겠습니다..
다음으로 Group의 Property를 알아보겠습니다.
subitems의 경우 초기화 구문에서도 할 수 있는 그룹에 아이템을 넣을 수 있습니다.
supplementaryItems는 그룹에 badge, header 등을 넣을 수 있습니다. SupplementaryItem의 자세한 내용은 아래에서 다시 설명하겠습니다.
interItemSpacing은 그룹 내 아이템의 간격을 설정할 수 있습니다.
NSCollectionLayoutEdgeSpacing(leading: .fixed(10), top: .flexible(10), trailing: nil, bottom: nil)
내부에. fixed는 위에서 썻던 .absolute와 같고, .flexible은 .estimated와 같습니다.
NSCollectionLayoutItem에도 edgeSpacing, contentInset과 같은 Property가 있어 아이템에서도 간격을 설정해줄 수 있습니다.
이번에는 NSCollectionLayoutSection을 알아보겠습니다.
생성은 처음 봤던 코드처럼 만들어 놓은 그룹을 NSCollectionLayoutSection에 넣어주면 끝입니다.
다음으로 Property를 보겠습니다. item, group보다 많은 Property를 가지고 있지만, 사용했던 몇 가지만 정리해보겠습니다.
- none - 활성화 안 된 상태
- continuous - 연속 스크롤
- continuousGroupLeadingBoundary - 그룹의 leading에 맞춰 자연스럽게 멈추는 연속 스크롤
- paging - 페이지 넘기듯 넘어가는 스크롤
- groupPaging - 한 그룹씩 페이징 스크롤
- groupPagingCentered - 각 그룹의 중심을 기준으로 페이징 스크롤
여기에 DecorationItem과 SupplementaryItem은 NSCollectionLayoutItem을 상속받아 만들어졌습니다.
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"
}
위와 같은 종류로 나뉘어 있는데 background는 decorationItem으로 생성할 수 있고, 나머지는 SupplementaryItem으로 생성이 가능합니다. 그리고 ElementKind에 String 값은 정해진 게 아니라 정하는 것이어서 어떤 값이든 통일되게만 사용하면 됩니다.
이 정도로 알아보고 실습을 해보겠습니다.
이런 화면을 만들어 보려고 CollectionView를 제대로 공부해봤습니다
Section, Badge의 경우 만드는 코드는 간단합니다.
// create badge
private func createGroupBadge() -> NSCollectionLayoutSupplementaryItem {
let layoutSize = NSCollectionLayoutSize(widthDimension: .estimated(24), heightDimension: .estimated(24))
let anchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: CGPoint(x: 0, y: -1))
let badge = NSCollectionLayoutSupplementaryItem(layoutSize: layoutSize, elementKind: ElementKind.badge, containerAnchor: anchor)
return badge
}
// sectionHeader Layout settings
private func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(30))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: ElementKind.sectionHeader, alignment: .top)
return sectionHeader
}
Layout을 만들면서 item에 Badge를 넣고 section에 Header를 넣어 준 채로 반환하는 함수를 만들었습니다.
private func boxOfficeSection() -> NSCollectionLayoutSection {
// layout
let layoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(0.8))
// item에 뱃지 추가
let item = NSCollectionLayoutItem(layoutSize: layoutSize,supplementaryItems: [createGroupBadge()])
item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 5, bottom: 0, trailing: 5)
// group
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(500), heightDimension: .estimated(200))
// deprecate 예정..
//let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// section
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 15, trailing: 5)
let sectionHeader = createSectionHeader()
//헤더 추가
section.boundarySupplementaryItems = [sectionHeader]
return section
}
만들어진 section을 Layout에 감싸 보내주면 끝입니다.
collectionView.register(BoxOfficeCell.self, forCellWithReuseIdentifier: "BoxOfficeCell")
collectionView.register(CollectionViewHeader.self, forSupplementaryViewOfKind: ElementKind.sectionHeader, withReuseIdentifier: "CollectionViewHeader")
collectionView.register(BoxOfficeBadge.self, forSupplementaryViewOfKind: ElementKind.badge, withReuseIdentifier: "BoxOfficeBadge")
collectionView.collectionViewLayout = layout()
private func layout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(section: boxOfficeSection())
}
layout이 다르지 않고 똑같은 것을 사용한다면 바로 생성해서 보내면 되지만 다양한 layout을 사용해야 할 때는 CompositionalLayout의 생성자에 Closure를 사용하면 됩니다.
private func layout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout {[weak self] sectionNumber, environment -> NSCollectionLayoutSection? in
switch self?.contents[sectionNumber].sectionType {
case .main:
return self?.createMainTypeSection()
case .basic:
return self?.createBasicTypeSection()
case .large:
return self?.createLargeTypeSection()
case .rank:
return self?.createRankTypeSection()
}
}
}
추가로 cell의 경우는 cellForItemAt를 통해 생성하지만 SupplementaryItem은 이 함수를 사용해 collectionView에 넣을 수 있습니다.
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
당연한 얘기겠지만 반환형이 UICollectionReusableView기 때문에 supplementaryItem들은 이걸 상속받아 만들어져야 합니다.
register 할 때 사용된 String이 kind에 들어오기 때문에 그걸 구분해서 헤더, 뱃지 등을 할당하고 반환해주면 됩니다.
제가 만들어본 프로젝트는 여기서 확인할 수있습니다.
https://github.com/JustHm/PocketMovie
layout만 간단하게 사용하는 방법을 공부해봤는데, collectionView를 이용해 여러 가지를 만들어 놓은 예제도 있어 이 예제를 보면서 추가적인 부분을 더 공부해야겠다.