맛동산이

Swift) CollectionView CompositionalLayout-2 본문

앱/Swift

Swift) CollectionView CompositionalLayout-2

진ddang 2024. 3. 2. 17:03

UICollectionVIewCompositionalLayout

Untitled.png

복잡한 컬렉션뷰의 중첩을 하여 만드는 레이아웃의 경우에는 IOS 13부터 compositionalLayout을 통해서 해결하였다. 컴포지셔널 레이아웃은 크게 섹션, 그룹, 그리고 아이템으로 나뉜다.

Untitled.png

테이블뷰는 다음과 같이 구성되어있다. 각 섹션마다 안에 그룹이 있고 그 그룹안에 각각의 아이탬이 존재한다.

Section

NSCollectionLayoutSection을 이용합니다. Group을 담는 Container입니다. Collection View는 하나 또는 여러 개의 Section을 가질 수 있습니다. Section은 Layout의 각각의 영역을 나타냅니다. Section은 NSCollectionLayoutGroup에 의해 결정됩니다. 각 Section은 고유의 배경, Header, Footer를 가질 수 있습니다.

Group

NSCollectionLayoutGroup을 이용합니다. Item을 담는 Container입니다. NSCollectionLayoutItem을 상속받았기 때문에 LayoutItem과 유사하게 동작합니다. Item을 특정 Path에 따라 배치하는 역할을 합니다. Group 자체는 레이아웃만 배치하고 렌더링은 하지 않습니다. 이 Group은 Section의 init(group:)에 주입됩니다. Group은 NSCollectionLayoutDimension을 이용하여 크기를 설정할 수 있습니다. 이에 대해선 아래에 설명을 적겠습니다.

Item

NSCollectionLayoutItem을 이용합니다. Collection View의 가장 기본 컴포넌트입니다. Item은 크기, 개별 content의 size, space, arragnge를 어떻게 할지에 대한 blueprint입니다. 일반적으로 Item은 Cell이지만 Headers, Footers, Decorations와 같은 Supplementary View도 될 수 있습니다. 마찬가지로 NSCollectionLayoutDimension로 크기를 결정합니다. 이 Item은 Group에 주입됩니다.

결과적으로 코드의 순서는

iteamsize→Item→groupsize→group→section 으로 값을 넣어주게 된다.

따라서 item은 상대적인 사이즈를 넣게 되면 group의 사이즈에 영향을 받게되고, group또한 section의 사이즈를 영향을 받게된다.

Iteam의 사이즈를 정해주기(NSCollectionLayoutDimension)

Untitled.png

CollectionView size는 NSCollectionLayoutDimension를 통해서 정해주며 크게 3가지 방법이 있다.

1. absolute

말그대로 고정된 픽셀값을 주는 방법이다.

let absoluteSize = NSCollectionLayoutSize(widthDimension: .absolute(44),
                                         heightDimension: .absolute(44))

2. estimate

let estimatedSize = NSCollectionLayoutSize(widthDimension: .estimated(200),
                                          heightDimension: .estimated(100))

3. fractional

자신이 속한 그룹의 사이즈(상위 뷰의)에 비율에 따라서 사이즈가 정해진다.

1이면 100%, 0이면 0%의 사이즈.

let fractionalSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                           heightDimension: .fractionalWidth(0.2))

UICollectionViewCompositionalLayout만들기

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
}
  • extension으로 따로 정리해서 메소드를 추가할수도 있다.

아이템사이즈 → 아이템 → 그룹사이즈 → 그룹 → 섹션 → 레이아웃 반환


실제 순서

1. 콜렉션뷰 만들고 ui위치 잡기

여타 다른 것들이랑 동일하게 그냥 오토레이아웃 먼저 잡아줌.

2. 콜렉션뷰의 IBOutlet 잡아주기

Untitled.png

3. xib파일 생성

empty파일로도괜찮고, cocoapod 파일로 xib include해서 만들어도 됨.

만들고나서 command + shift + L 로 collectionViewCell을 가져옴.

Untitled.png

그리고 class연결해줌.

연결해주고나서 필요한 ui는 outlet잡아주기.

4. nib파일 Register해주기

let nibName = String(describing: CollectionViewCell.self)
let myTableNib = UINib(nibName: nibName, bundle: nil)
self.collectionView.register(myTableNib, forCellWithReuseIdentifier: nibName)

5. datasource, delegate 추가해주기

Untitled.png

추가해주면 당연히 해당 뷰 컨트롤러와 delegate, datasource도 연결해줘야한다.

Untitled.png

물론 해당 익스텐션에

6. 컴포지셔널 레이아웃 잡아주기

그냥해도 되지만 정리를 위해서 mark로 잡아주고 extension으로 함수추가.

Untitled.png

Untitled.png

그러고나서 viewDidLoad에 추가.

NSCollectionLayoutGroup

.vertical

.horizental

orthogonalScrollingBehavior(섹션별 스크롤방향)

섹션에서 수평 스크롤을 넣고 싶은경우에 해당 메소드를 사용하게 된다.

section.orthogonalScrollingBehavior = .continuous

이러한 방식으로, 처리하게 되는데

총 5개의 속성이 존재한다.

// Standard scroll view behavior: UIScrollViewDecelerationRateNormal
case continuous

// Scrolling will come to rest on the leading edge of a group boundary
case continuousGroupLeadingBoundary

// Standard scroll view paging behavior (UIScrollViewDecelerationRateFast) with page size == extent of the collection view's bounds
case paging

// Fractional size paging behavior determined by the sections layout group's dimension
case groupPaging

// Same of group paging with additional leading and trailing content insets to center each group's contents along the orthogonal axis
case groupPagingCentered

페이징은 아이탬 갯수만큼의, 그룹이 한번씩 넘어가는 마치 책을 넘기듯이 하는 모션을 의미하며

continuous는 아이템이 자연스럽게 수평으로 움직이는 우리가 평소에 생각하는 스크롤을 생각하면 된다.

예제코드

let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1)))
item.contentInsets = .init(top: 0, leading: 5, bottom: 16, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.9), heightDimension: .estimated(200)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
	section.boundarySupplementaryItems = [.init(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), elementKind: catgoryHeaderId, alignment: .topLeading)]
	section.orthogonalScrollingBehavior = .groupPaging
	section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
return section

contentInsets를 통해서 cell간의 간격을 주게된다.

섹션별로 다른 데이터를 주고싶을때

섹션을 나누긴 했으나, 스토리보드에서는 섹션을 나누기도 애매하고, 어떻게 해야할지도 몰랐다. 일단 오토레이아웃이 어떻게 들어가지? 라고 생각했는데 이는 바보같은 질문이었다.

생각해보면, 최초에 콜랙션뷰의 오토레이아웃을 잡고 그 안에 들어가는 아이템의 정렬을 우리가 collectionViewCompositionalLayout을 통해서 잡아주는것이기 때문에 그안에 들어가는 아이템의 사이즈만 정해주면 알아서 collectionView안에서 자리잡게 된다.

최초기준이 collectionView라는것.

그러면 section을 나누거나, 하는것은 어떻게 하나요????????(db에 저장해두면된다. 정보를 받아오고, 그것을 보여주는 식이기 때문에 이때 section에 대한 정보를가지고 있다면, 해당 섹션에 대한 정보만을 넣어줄수 있게 되는것이다. )

간단하게 말하면 collectionViewCompositionalLayout을 통해서 섹션, 그룹, 아이템의 사이즈를 다 정해주고 섹션에 대한 정렬은 db에서 해주는것.

아무튼 코드를 보면 이해가 갈것이다.

bookmark

static func createLayout() -> UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
            if sectionNumber == 0 {
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1)))
                          let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.9), heightDimension: .estimated(200)), subitems: [item])
                          group.contentInsets = .init(top: 0, leading: 5, bottom: 16, trailing: 5)
                          let section = NSCollectionLayoutSection(group: group)
                          section.boundarySupplementaryItems = [.init(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), elementKind: catgoryHeaderId, alignment: .topLeading)]
                          section.orthogonalScrollingBehavior = .groupPaging
                          section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
                          return section
            } else if sectionNumber == 1 {
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1)))
                           item.contentInsets = .init(top: 0, leading: 5, bottom: 16, trailing: 5)
                           let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.45), heightDimension: .estimated(200)), subitems: [item])
                           let section = NSCollectionLayoutSection(group: group)
                           section.boundarySupplementaryItems = [.init(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), elementKind: catgoryHeaderId, alignment: .topLeading)]
                           section.orthogonalScrollingBehavior = .groupPaging
                           section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
                           return section
            } else {
                let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
                item.contentInsets = .init(top: 0, leading: 5, bottom: 16, trailing: 5)
                let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(0.9), heightDimension: .estimated(300)), subitem: item, count: 5)
                let section = NSCollectionLayoutSection(group: group)
                section.boundarySupplementaryItems = [.init(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), elementKind: catgoryHeaderId, alignment: .topLeading)]
                section.orthogonalScrollingBehavior = .groupPaging
                section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
                return section
            }
        }
    }

섹션별로 다른 데이터를 사용해야 하는 경우

섹션별로 다른데이터를 사용해야하지만, 이러한 경우에 배열을 두개를 사용하게 되면 해당 배열의 전체 데이터 크기 혹은 데이터별로 사이즈를 확인할수 없게된다.

이러한 경우에는 protocol을 통해서 추상화를 하는것으로 데이터를 하나의 프로토콜로 묶고, 데이터 바인딩을 해주는경우에 다운캐스팅을 통해서 데이터를 적용해주면 된다.

해당코드를 참고해보자.

github :

link_preview


bookmark

bookmark

bookmark

반응형