Compositional Layout에 대하여

SOPT·2022년 12월 28일
10

iOS

목록 보기
1/2
post-thumbnail
post-custom-banner

세미나, 합동 세미나, 스터디…… SOPT에서 많은 활동을 하면서 이전과는 비교도 안 되는 복잡한 뷰들을 많이 만들어 봤던 것 같아요.

스크린샷 2022-12-23 오후 12 04 00

그 중 하나가 이 뷰였는데요. 여러분들은 이 뷰를 어떻게 구현하시나요? 저는 전체 tableView를 만들고, 각 tableViewCell들에 collectionView를 넣는 방법을 사용했어요. 혹은 첫 번째와 세 번째는 header, footer로 구현하면 그래도 고생은 덜하지 않을까? 이런 고민을 했었던 기억이 나요.

열심히 tableViewCell을 만들고, 그 안에 다시 collectionView를 넣던 와중에 문득 이런 의문이 들었어요. 아니 왜 이렇게 비효율적인 짓을 하지… 요즘 시대에 뭐라도 만들어 놨어야 하는 거 아닌가… 라는 생각에 검색을 해 보니 아니나 다를까 이미 Compositional Layout 이라는 게 나와 있더라구요?

(단, iOS 13 이상만 지원 💦)

어쨌든 제가 당장 공부해 봤습니다 ~ 😎



Compositional Layout의 등장

스크린샷 2022-12-23 오후 12 06 08

당장 앱스토어만 봐도 요즘 앱의 디자인들이 얼마나 복잡한지 알 수 있어요. 앱스토어를 tableView와 collectionView가 중첩되는 뷰로 구현해야 한다고 생각하면 정말 머리가 아픕니다. 이를 보완하기 위해 flexible하고 빠르게 어떤 레이아웃이든 만들 수 있는 compositional layout이 탄생하게 됩니다.


구성

스크린샷 2022-12-23 오후 12 07 16

Compositional Layout은 item, group, section, layout으로 구성됩니다. 하나의 layout에 section, section 안에 group, group 안에 item들이 있어요. 각 요소들의 size를 정하고 지정해 주기만 하면 레이아웃이 완성돼요.


크기

스크린샷 2022-12-23 오후 12 08 07

각 요소들의 size를 정해 주는 방법은 3가지가 존재합니다.

  • fractionalWidth & fractionalHeight - 컨테이너와의 너비&높이 비율
  • absolute - 포인트값으로 지정
  • estimated - 후에 content의 크기가 바뀌어 크기가 정확하지 않을 때는 estimate 값으로

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                     heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

item은 group 안에 존재해요. 따라서 item의 size는 group과의 비율로 나타내게 됩니다. 그렇다면 이 코드에서 item의 너비는 group 너비의 20%, 높이는 group 높이의 100%가 되겠죠. 그리고 이러한 size로 item을 구성하겠다고 지정해 주기만 하면 됩니다. group과 section 모두 이런 식으로 구성하면 됩니다.


item

스크린샷 2022-12-23 오후 12 08 53

Group

스크린샷 2022-12-23 오후 12 09 06

지정된 방식에 따라 item들을 배치합니다. 3가지 방식이 존재합니다.

  • horizontal
  • vertical
  • custom ⇒ custom하여 아이템의 absolute 크기와 위치 지정 가능

Section

스크린샷 2022-12-23 오후 12 09 19

Layout

스크린샷 2022-12-23 오후 12 10 38
  • iOS, tvOS - UICollectionViewCompositionalLayout
  • macOS - NSCollectionViewCompositionalLayout
  • 플랫폼에 따라 이름은 다르지만 정의는 동일합니다.
  • provider 클로저로 섹션마다 다양한 레이아웃을 정의할 수 있습니다. ⇒ 이제 섹션별 레이아웃이 완전히 구별될 수 있기 때문에 많은 가능성이 열립니다.

예제 2개를 살펴보고 마무리 하겠습니다.

Example - Grid

Compositional Layout을 사용하여 Grid 형태를 구현해 볼게요.

스크린샷 2022-12-23 오후 12 12 06
    private func createLayout() -> UICollectionViewLayout {
        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)
        
        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
    }
  1. item은 group 안에 존재합니다.
    따라서 item의 너비 = container(group) 너비의 20%
    item의 높이 = container(group) 높이와 동일(100%)
  1. group은 section 안에 존재합니다.
    group의 너비 = container(section) 너비와 동일(100%)
    group의 높이 = container(section) 너비의 20%(item이 정사각형이 됨)
  1. item에 Inset도 추가해서 구현해 보았습니다.

Example - 여러 개의 Section

스크린샷 2022-12-23 오후 12 14 38

여러 개의 section을 가진 뷰를 만들어 볼게요.

    enum SectionLayoutKind: Int, CaseIterable {
        case list, grid1, grid2
        
        var columnInt: Int {
            switch self {
            case .list:
                return 1
            case .grid1:
                return 5
            case .grid2:
                return 2
            }
        }
    }

섹션이 여러 개이므로, enum으로 만들어 관리해 줍니다. columnInt는 각 section에 나타낼 열의 개수를 표시합니다. list 섹션의 column은 1이 되고, grid1의 열은 5개, grid5의 열은 2개가 되도록 했습니다.

 private func createLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else {return nil}
//            print(sectionLayoutKind)
            let columns = sectionLayoutKind.columnInt
//            print(columns)
            
            var itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: .fractionalHeight(1.0))
            if columns == 5 {
                itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                                  heightDimension: .fractionalHeight(1.0))
            } else if columns == 2 {
                itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                  heightDimension: .fractionalHeight(1.0))
            }
            
            
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
            
            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, repeatingSubitem: item, count: columns)
            
            let section = NSCollectionLayoutSection(group: group)
            
            return section
        }
        
        return layout
        
    }
  1. provider 클로저로 섹션마다 다양한 레이아웃을 정의할 수 있다고 앞서 언급했었는데요. 이번 예제 같은 여러 개의 섹션을 구현할 때 사용됩니다.
init(sectionProvider: UICollectionViewCompositionalLayoutSectionProvider)
  1. item
    list 형태일 때는 group의 너비, 높이와 동일하게 지정합니다.
    grid1일 때는, 열이 5개가 되도록 너비가 group 너비의 20%가 되도록 합니다.
    grid2일 때는, 열이 2개가 되도록 너비가 group 너비의 50%가 되도록 합니다.
    마찬가지로 item에 inset을 주었습니다.
  1. 결과 이미지를 보면 각 section의 group마다 높이가 다릅니다. 이를 고려하여 groupHeight 상수를 만들어 열이 1개일 시와 아닐 때로 나누어 size를 지정해 주었습니다.


이런 식으로 레이아웃이 완전히 바뀌어도 레이아웃을 정의하는 코드 자체는 크게 변화하지 않는다는 게 정말 흥미로운 점 같아요.
WWDC를 보면 더 많은 예제들과 DataSource를 사용하는 새로운 방법에 대한 설명도 있어요.
관련 링크를 두고 이만 글을 마치겠습니다. 👋🏻


💻 WWDC 19 - Advances in Collection View Layout
💻 WWDC 19 - Advances in UI Data Sources
💻 WWDC 20 - Advances in UICollectionView

작성자
IN SOPT, YB iOS 김민

profile
IT 대학생벤처창업동아리 SOPT의 공식 블로그입니다.
post-custom-banner

3개의 댓글

comment-user-thumbnail
2022년 12월 28일

와 진짜 멋지다

답글 달기
comment-user-thumbnail
2022년 12월 28일

와 진짜 멋지다

답글 달기
comment-user-thumbnail
2023년 5월 15일

와 이거 읽으면서 진짜 대박이당.. 했는데 작성자가 민니 언니네? 체고야 언니...🤍

답글 달기