본문 바로가기

Mobile/iOS

[iOS] CollectionView - CompositionalLayout(1)

반응형


1. Layout

Layout은 CollectionVIew의 Content들의 구성을 결정합니다. Layout은 개발자가 원하는 대로 Content를 구성할 수 있게 유연하게 디자인되어 있습니다. 아래 사진에 나와있는 App Store의 Layout도 Custom된 Layout으로 구성되어 있습니다.


2. Compositional Layout

CollectionView는 두 가지 방식의 Layout을 제공하고 있습니다. 

  • FlowLayout
  • CompositionalLayout

그 중에서 제가 공부해 볼 건 CompositionalLayout 입니다. CompositionalLayout은 빠르고 유연하게 다양한 형태로 뷰를 구성합니다. ConpositionalLayout은 Section, Group, Item으로 구성되어 있습니다. 아래 이미지를 보시겠습니다.

하나의 섹션 안에 여러 개의 그룹이 들어 갈 수도 있고, 하나의 그룹 안에 아이템도 여러 개 들어갈 수 있습니다. 섹션은 그룹의 모음, 그룹은 아이템의 모음이라고 생각할 수 있습니다. 아래는 CollectionViewLayout으로 구성한 기본적인 ListLayout 입니다.

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
}

item의 크기가 결정되면, group은 item 배열을 받아 그 크기를 구성하고, 다시 섹션은 그룹을 받아 전체 Layout을 결정합니다.


3. Sample App 살펴보기

Sample App은 아래에서 다운받아서 실행하시면 됩니다.

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

 

Implementing Modern Collection Views | Apple Developer Documentation

Bring compositional layouts to your app and simplify updating your user interface with diffable data sources.

developer.apple.com

(1) Create a Grid Layout

해당 예시는 Gird 형태의 Layout을 보여주고 있다. 가로로는 아이템이 5개씩 배치가 되어 있고, 세로로는 스크롤이 가능한 배치이다. 어떻게 하면 위와 같이 설정할 수 있을까? 코드를 한번 살펴보도록 하자.

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)
return layout

위 코드는 fractional sizing을 통해서 한 행에 같은 사이즈의 item을 다섯 개 넣을 수 있는지를 보여준다.

CollectionView Size 관련
- .absolute : 고정 크기
- .estimated : 런타임에 변경
- .fractional : 비율

우선 위에 비율로 보기에는 처음 하는 사람에게는 이해하기 어려울 수 있다. 아래와 같이 고정된 크기로 놓고 비교해보도록 하자.

let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(300),
                                     heightDimension: .absolute(300))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(300),
                                       heightDimension: .absolute(300))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                                 subitems: [item])

현재 그룹과 아이템의 사이즈는 300 * 300 으로 같은 크기입니다. 즉 현재 그룹에는 한 개의 아이템이 속해있는 셈입니다. 그럼 여기서 아이템의 너비를 절반인 150으로 줄이면 어떻게 변할까요?

자 ~ 보시면 아이템의 너비가 줄어 하나의 구룹에 두 개의 아이템이 들어 간 것을 볼 수 있습니다. 여기서 너비를 줄이면 하나의 그룹에 포함되는 아이템들이 계속해서 늘어나겠죠? 마찬가지로 그룹의 크기를 늘리거나 줄이면 아이템이 늘거나 줄어들 수 있습니다. 이 부분은 직접해보시면 금방 아실 수 있으실거에요.

이것을 비율로 조절하면 조금 더 쉽게 layout을 구성할 수 있습니다. 고정된 크기로 놓게 되면 이미지를 배치했을 때 잘린 이미지가 될 수 있기 때문입니다. 이것을 한번 정리해볼까요?

  • Group: 그룹은 아이템을 감싸는 Frame입니다. 안에 들어가는 아이템은 아이템의 크기, 그룹의 크기에 따라 가변적입니다.
  • Group의 vertical / horizontal : Group 내의 아이템들을 배치합니다. vertical의 경우 아이템을 세로로 배치하고 horizontal의 경우 아이템을 가로로 배치합니다. vertical의 경우 Group의 높이와 아이템의 높이의 영향을 받고, horizontal의 경우는 너비에 영향을 받습니다.
  • Item: Group 안에 들어가는 아이템의 너비와 높이를 결정합니다.
  • Group의 설정이 horizontal일 때, Group의 너비가 넓고 Item의 너비가 좁을수록 더 많은 아이템이 하나의 Group에 포함됩니다.

(2) Add Spacing Around Items

아래와 같이 옵션을 넣으면 아이템 주변에 공간을 줄 수 있습니다.

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                     heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

(3) Create Column Layout

아래와 같이 count를 넣어주면 그룹의 너비를 정해주지 않아도 컬럼을 나눠줄 수 있다. 

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, subitem: item, count: 2)
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)

count를 2로 주고 Width의 비율을 0.5로 줄여주면 아래 예시와 같은 모습으로 보인다.

(4) Display Distinct Layouts Per Section

위와 같이 같은 콜렉션 뷰에서 섹션에 따라 다른 구성으로 만들 수도 있습니다. sectionIndex를 받아서, sectionIndex에 따라 컬럼의 형태를 다르게 만들 수 있습니다.

 enum SectionLayoutKind: Int, CaseIterable {
        case list, grid5, grid3
        var columnCount: Int {
            switch self {
            case .grid3:
                return 3

            case .grid5:
                return 5

            case .list:
                return 1
            }
        }
    }

 

let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
    layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in

    guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }
    let columns = sectionLayoutKind.columnCount

    // The group auto-calculates the actual item width to make
    // the requested number of columns fit, so this widthDimension is ignored.
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                         heightDimension: .fractionalHeight(1.0))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)

    let groupHeight = columns == 1 ?
        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

데이터 소스도 마찬가지로 SectionLayoutKind에 따라서 바꿔주면 됩니다.

func configureDataSource() {
        
        let listCellRegistration = UICollectionView.CellRegistration<ListCell, Int> { (cell, indexPath, identifier) in
            // Populate the cell with our item description.
            cell.label.text = "\(identifier)"
        }
        
        let textCellRegistration = UICollectionView.CellRegistration<TextCell, Int> { (cell, indexPath, identifier) in
            // Populate the cell with our item description.
            cell.label.text = "\(identifier)"
            cell.contentView.backgroundColor = .cornflowerBlue
            cell.contentView.layer.borderColor = UIColor.black.cgColor
            cell.contentView.layer.borderWidth = 1
            cell.contentView.layer.cornerRadius = SectionLayoutKind(rawValue: indexPath.section)! == .grid5 ? 8 : 0
            cell.label.textAlignment = .center
            cell.label.font = UIFont.preferredFont(forTextStyle: .title1)
        }
        
        dataSource = UICollectionViewDiffableDataSource<SectionLayoutKind, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
            // Return the cell.
            return SectionLayoutKind(rawValue: indexPath.section)! == .list ?
            collectionView.dequeueConfiguredReusableCell(using: listCellRegistration, for: indexPath, item: identifier) :
            collectionView.dequeueConfiguredReusableCell(using: textCellRegistration, for: indexPath, item: identifier)
        }

        // initial data
        let itemsPerSection = 10
        var snapshot = NSDiffableDataSourceSnapshot<SectionLayoutKind, Int>()
        SectionLayoutKind.allCases.forEach {
            snapshot.appendSections([$0])
            let itemOffset = $0.rawValue * itemsPerSection
            let itemUpperbound = itemOffset + itemsPerSection
            snapshot.appendItems(Array(itemOffset..<itemUpperbound))
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }
반응형