iyOmSd/Title: Swift

[Swift] UICollectionViewCompositionalLayout

냄수 2021. 4. 27. 18:22
반응형

WWDC19에서 소개된 내용이에요

CollectionView의 Layout을 커스텀하기 쉽게 만들어주는 역할을 해줘요

iOS13+ 에서 사용할 수 있는 API구요

 

들어가기앞서 이 레이아웃을 사용하기위해서 기본적으로 알아야할 개념들이있어요

 

NSCollectionLayoutSize

NSCollectionLayoutItem

NSCollectionLayoutGroup

NSCollectionLayoutSection

 

여기에 추가적으로

NSCollectionLayoutSupplementaryItem

NSCollectionLayoutBoundarySupplementaryItem

까지!

(이부분은 헤더뷰 푸터뷰 필요없으면 안보셔도 OK)

 

 

왜알아야해...?

UICollectionViewCompositionalLayout를 만들기위해서는

NSCollectionLayoutSection을  필요로하구요

 

NSCollectionLayoutSection을 만들기위해서는

NSCollectionLayoutGroup이 필요하고...

 

NSCollectionLayoutGroup은 NSCollectionLayoutItem를 상속받아 만들어졌어요

아이템이 모인 그룹이니까 아이템이라고 볼 수도 있죠

NSCollectionLayoutGroup을 만들기위해선 NSCollectionLayoutItem이 필요하죠

 

 

 

이렇게 모두 연결되어있기 때문에 다 알아야해요 ㅎㅎ

 

레이아웃은

이런형태로 구성되어있어요

 

 

NSCollectionLayoutSize

let size = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .fractionalHeight(0.2))

셀의 크기를 결정해주는 요소에요

옵션으로

 

absolute - 고정 크기

fractionalHeight - 포함된 그룹안의 세로비율만큼의 크기(최대 1)

fractionalWidth - 포함된 그룹안의 세로비율만큼의 크기(최대 1)

estimated - 최소크기

 

를 나타내고

포함된 그룹안의 ~비율만큼크기?

 

포함된 뷰가 200 x 100 크기라면

fractionalWidth(0.2)를 적용한다면

뷰의 가로크기는 200이니까 200 * 0.2 -> 40크기를 받겠네요

비율에 맞게 늘어나고 줄어들도록 할 수 있어요

 

NSCollectionLayoutItem

let item = NSCollectionLayoutItem(layoutSize: size)

말 그대로 아이템을 나타내요

위에서 만든 size를 적용한 Cell 이죠

 

NSCollectionLayoutGroup

NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 4)
                                                     
NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [leadingItem, trailingGroup])

가로와 세로를 기준으로 만들수 있고 또는 커스텀으로 만들 수 있어요

같은 크기의 셀을 반복하느냐

다른 크기의 셀을 반복하느냐 그런 차이가있죠

 

 

let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView.collectionViewLayout = layout

그룹넣어주고 섹션넣어주고.. 

레이아웃 적용하면 끗!

 

 

적용을 해봐야겠죠?!

 

.

.

.

.

 

간단예제

 

우선 간단한 예제부터 시작해보려구요

let size = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .fractionalHeight(0.2))
let item = NSCollectionLayoutItem(layoutSize: size)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 4)

let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView.collectionViewLayout = layout


extension ViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 3
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = colors[indexPath.item % colors.count]
        return cell
    }
}

위의 코드를 적용시킨결과를 살펴볼게요

우선 섹션 2개니까 반복되는게 2개가 보이구요

 

Cell Size == NSCollectionLayoutSize 라고했잖아요?

 

셀크기는 가로 100고정

세로는 그룹세로길이의 0.2

얼추맞는것 같죠?

 

그룹의 크기는

컬렉션뷰 가로길이의 1

컬렉션뷰 세로길이의 0.5

딱 맞네요!

 

그룹의 아이템으로는

위에서 적용한 Cell Size를

가로로 4개까지 포함 되도록했어요

그래서 지금 3개의 셀이기때문에 한칸이 비어있네요

 

이상태에서 만약 5개의 셀이 있다면

그룹이 아래에 하나 더 생기겠죠

아래처럼요

 

핑크색선이 그룹을 나타내고

갈색선이 섹션을 나타내요

 

벳지

NSCollectionLayoutSupplementaryItem

문서의 설명으론

벳지나 프레임같이 시각적 장식을 추가하는데 사용되는 객체 라고 써잇어요

궁금해서 써보려구요 ㅎㅎ

 

먼저 

NSCollectionLayoutAnchor

라는게 있는데

Cell의 어디에 붙을거니? 를 정해주는거에요

let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: .init(x: 0.3, y: -0.3))

 

 

그다음 벳지의 사이즈를 정해주고

아이템을 만들어주면 되요

let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20), heightDimension: .absolute(20))


let badge = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind: "badge", containerAnchor: badgeAnchor)

let badgeItem = NSCollectionLayoutItem(layoutSize: size, supplementaryItems: [badge])

이렇게 만든

벳지가 붙은 아이템을

적용하려면 어떻게해야할까요

 

먼저 벳지뷰를 담당할

UICollectionReusableView Class를 하나 생성해주세요

 

 

그리고

등록을 해줘야해요

class BadgeView: UICollectionReusableView {}

collectionView.register(BadgeView.self, forSupplementaryViewOfKind: "badge", withReuseIdentifier: "BadgeView")

 

 

그다음

DataSource에 있는 함수에서 벳지에 대한 정의를 해줘야해요

저는 빨간색을 적용하고

동그랗게 해줬어요

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == "badge" {
        let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "BadgeView", for: indexPath)
        view.backgroundColor = .red
        view.layer.cornerRadius = 10
        return view
    } else {
        return .init()
    }
}

 

 

코드 변경사항은 item을

badgeItem으로 바꿔주기만했어요

 

헤더뷰 푸터뷰 

NSCollectionLayoutBoundarySupplementaryItem

라는 클래스를 사용해요

위에서 본

NSCollectionLayoutSupplementaryItem 와는 다른 용도로 사용돼요

헤더뷰나 푸터뷰의 레이아웃을 추가할 때 사용하는 객체구요

 

벳지를 사용할 때와 사용법은 비슷해요

 

등록을먼저해주고

collectionView.register(HeaderView.self,
                        forSupplementaryViewOfKind: "header",
                        withReuseIdentifier: "HeaderView")
collectionView.register(FooterView.self,
                        forSupplementaryViewOfKind: "footer",
                        withReuseIdentifier: "FooterView")

 

헤더와 푸터의 사이즈를 지정해줘요

let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(44))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                         elementKind: "header",
                                                         alignment: .top)
let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(22))
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize,
                                                         elementKind: "footer",
                                                         alignment: .bottom)
// 헤더뷰가 계속 떠잇도록 해주는 프로퍼티
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header, footer]

마지막에 section에 적용해줘야하구요

 

 

헤더뷰에 Label을 달아줘밨어요

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        if kind == "badge" {
            let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "BadgeView", for: indexPath)
            view.backgroundColor = .red
            view.layer.cornerRadius = 10
            return view
        } else if kind == "footer" {
            let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterView", for: indexPath)
            view.backgroundColor = .gray
            return view
        } else if kind == "header" {
            let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderView", for: indexPath)
            view.backgroundColor = .purple
            
            let label: UILabel = {
                $0.text = "헤더뷰"
                $0.textColor = .white
                return $0
            }(UILabel())
            view.addSubview(label)
            label.translatesAutoresizingMaskIntoConstraints = false
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            return view
        } else {
            return .init()
        }
    }

 

 

 

 

Nested CollectionVIew 중첩 레이아웃

중첩된 그룹을 사용해서

여러 레이아웃이 적용될 수 있는 컬렉션뷰를 해볼거에요

 

이런 레이아웃을 한번 구현해볼까요

 

 

 

코드를 먼저 보고 설명할게요

// 상단 왼쪽 셀
let leadingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
                                         heightDimension: .absolute(100))
let leadingItem = NSCollectionLayoutItem(layoutSize: leadingSize)

// 상단 오른쪽 셀
let trailingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                          heightDimension: .absolute(100))
// absolute가 먹히질않는 이슈가있음.. 고정되지않고 group의 크기에 맞게 늘어남
let trailingItem = NSCollectionLayoutItem(layoutSize: trailingSize)
let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
                                               heightDimension: .fractionalHeight(0.5))
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize,
                                                     subitem: trailingItem,
                                                     count: 3)
                                                   
// 상단 그룹
let topGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                          heightDimension: .fractionalHeight(0.5))
let topGroup = NSCollectionLayoutGroup.horizontal(layoutSize: topGroupSize,
                                                  subitems: [leadingItem, trailingGroup])

상단부터 구현할거에요

 

이렇게 생겼네요

 

왼쪽 부분과 오른쪽 부분을 나눠서 구현해볼거에요

왼쪽은 leadingItem

오른쪽은 trailingItem을 만들고

3개가 잇어야하니까 trailingGroup으로 만들어줄거구요

 

leadingItem과 trailingGroup을 

하나의 그룹으로 묶을거에요

묶어서 topGroup이라고 정의했고

가로로 스크롤될 수 있도록 구현했어요

 

크기를 조정하는 부분에서 absolute가 고정크기를 나타내는건데 먹히질 않더라구요.. 흠

그룹 크기에 따라 늘어나기도 줄어들기도해요

그룹이 있다면

그룹방향과 같은 쪽은 (세로방향 - 높이, 가로방향 - 넓이)

그룹방향사이즈 / count로 넓이를 균등하게 주는것같아요

 

 

다음으로 아래 부분!

작은 셀과

큰 셀이 보이네요

let bottomSmallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                 heightDimension: .fractionalHeight(0.3))
let bottomBigItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(0.7))
let bottomSmallItem = NSCollectionLayoutItem(layoutSize: bottomSmallItemSize)
let bottomBigItem = NSCollectionLayoutItem(layoutSize: bottomBigItemSize)

let bottomGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                             heightDimension: .fractionalHeight(0.5))
let bottomGroup = NSCollectionLayoutGroup.vertical(layoutSize: bottomGroupSize,
                                                   subitems: [bottomSmallItem, bottomBigItem])

 

비율에 맞도록

topGroup과 bottomGroup은 각각 반씩 차지하도록 0.5의 높이를 가지도록했어요

 

 

let containerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
                                                heightDimension: .fractionalHeight(1))
let containerGroup = NSCollectionLayoutGroup.vertical(layoutSize: containerGroupSize,
                                                      subitems: [topGroup, bottomGroup])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = .continuous

마지막으로

topGroup과 bottomGroup을 하나의 그룹으로 또 묶어주면

완성이에요

 

그리고 섹션 프로퍼티중

orthogonalScrollingBehavior

라는 게 있는데

스크롤 형식을 설정할 수 있어요

 

continuous - 일반적 이동가능

        continuousGroupLeadingBoundary - 무조건 전페이지의 leading으로이동

        groupPaging - 가까운 그룹으로 페이징효과(leading에 맞춰짐)

        groupPagingCentered - 그룹의 센터로 이동

        none - 스크롤 x

        paging - 컬렉션뷰기준 페이징

 

원하는 동작방식에 맞게 설정해주시면 됩니다!

 

이렇게 구현하면 셀은 어떤형식으로 보여질까요?

 

최종 그룹인

containerGroup이 vertical로 선언되었네요

그럼 세로로 만들어질거니까

containerGroup하나는 보라색 영역이 될거에요

containerGroup이 세로로 선언되서

topGroup

bottomGroup 순으로 cell이 생길거구요

 

더 많아진다면 

가로로 그룹이 추가될거에요

 

섹션을 2개로했다면 아래에 같은 레이아웃이 또있겟죠

 

아래는 전체코드에요

func nestedCollectionView() -> NSCollectionLayoutSection {
        let leadingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
                                                 heightDimension: .absolute(100))
        let leadingItem = NSCollectionLayoutItem(layoutSize: leadingSize)
        
        let trailingSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                  heightDimension: .absolute(100))
        // absolute가 먹히질않는 이슈가있음.. 고정되지않고 group의 크기에 맞게 늘어남
        let trailingItem = NSCollectionLayoutItem(layoutSize: trailingSize)
        let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
                                                       heightDimension: .fractionalHeight(0.5))
        let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize,
                                                             subitem: trailingItem,
                                                             count: 3)
        let topGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                  heightDimension: .fractionalHeight(0.5))
        let topGroup = NSCollectionLayoutGroup.horizontal(layoutSize: topGroupSize,
                                                          subitems: [leadingItem, trailingGroup])
        
        let bottomSmallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                heightDimension: .fractionalHeight(0.3))
        let bottomBigItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                heightDimension: .fractionalHeight(0.7))
        
        let bottomSmallItem = NSCollectionLayoutItem(layoutSize: bottomSmallItemSize)
        let bottomBigItem = NSCollectionLayoutItem(layoutSize: bottomBigItemSize)
        let bottomGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                     heightDimension: .fractionalHeight(0.5))
        let bottomGroup = NSCollectionLayoutGroup.vertical(layoutSize: bottomGroupSize,
                                                             subitems: [bottomSmallItem, bottomBigItem])
        
        let containerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
                                                        heightDimension: .fractionalHeight(1))
        let containerGroup = NSCollectionLayoutGroup.vertical(layoutSize: containerGroupSize,
                                                                subitems: [topGroup, bottomGroup])
        
        let section = NSCollectionLayoutSection(group: containerGroup)
        section.orthogonalScrollingBehavior = .continuous
        return section
    }

 

 

정렬된 코드인데 포스팅하면 코드위치가 어긋나네요..ㅠ..

 

 

 

 

 

 

 

반응형