[WWDC 19] Advances in Collection View Layout

민니·2022년 12월 6일
0

WWDC

목록 보기
1/3

마켓컬리 검색 뷰를 클론코딩하는 도중, 전체 tableView를 만들고 tableViewCell들 안에 또 collectionView를 넣는 나를 보고....
1. 이런 비효율적인 짓을 해야 하나? 라는 생각과...
2. 애플이 나보다 바보일 리가 없어......... 라는 생각이 들었고 역시나 Compositional Layout이라는 것이 있었음 🥲
Compositional Layout을 이해하기 위해, WWDC를 먼저 보기로!


Compositional Layout

복잡한 레이아웃을 쉽게 구현하고, 기존 custom layout의 단점들을 보완하여 나온 Compositional Layout

  • Composable: 단순한 것으로 복잡한 것을 만들 수 있음
  • Flexible: 어떠한 레이아웃이든 만들 수 있음
  • Fast: 빠르게 구현할 수 있음

item > group > section > layout
하나의 layout에 section, section 안에 group, group 안에 item들이 있음


예시로 코드를 살짝 본다면!

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)

이런 식으로 item, group, section, layout을 사용하여 compositional layout을 구현함


Core Concepts


NSCollectionLayoutSize


NSCollectionLayoutDimension

  • 너비와 높이의 치수는 스칼라값이 아닌 얘네를 사용함
  • fractionalWidth & fractionalHeight: 컨테이너와의 비율
  • absolute: 절대값으로도 당연히 가능
  • estimated: 후에 content의 크기가 변화해서 크기가 정확하지 않을 때는 estimate 값으로 줄 수도 있음

ex

container의 너비의 50%만큼의 너비

container의 너비, 높이의 25%

높이를 200point로 지정

높이를 200으로 estimate(추정)한 경우


NSCollectionLayoutItem

NSCollectionLayoutGroup

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

NSCollectionLayoutSection


UICollectionViewCompositionalLayout & NSCollectionViewCompositionalLayout

  • 최상위 레이아웃 클래스
  • iOS, tvOS - UICollectionViewCompositionalLayout
  • macOS - NSCollectionViewCompositionalLayout
  • 플랫폼에 따라 이름은 다르지만 정의는 동일
  • provider 클로저로 섹션마다 다양한 레이아웃을 정의할 수 있음이제 섹션별 레이아웃이 완전히 구별될 수 있기 때문에 많은 가능성이 열림

Demo

List

  • createLayout() : compositional layout 생성 함수 선언
  1. item과 item 크기
  2. 이 item에 대한 내용을 바탕으로 group 정의
    • group의 너비를 container 너비의 100%로 지정
    • group의 높이를 44 포인트로 지정
    • item의 너비와 높이를 container의 100%로 지정 (이때 container은 group!)
  3. 해당 group을 section으로 묶음
  4. 마지막으로 compositional layout을 만들고 return
  • 생각해 보면 궁극적으로 item의 크기를 결정하는 것은 그룹
  • compositional layout의 그룹은 일반적으로 item으로 구성된 열이나 행과 같은 반복 구조를 나타냄

💡 흥미로운 점은 다른 레이아웃을 구성하기 위해서 이 코드를 많이 변경할 필요가 없다는 점


Grid

  • grid에서는 item의 너비와 group의 너비가 같지 않고
  • 각 행을 5개의 item으로 구성하고 싶음
  • 그룹이 행을 의미하므로, item의 크기를 지정할 때 .fractionalWidth(0.2) 코드 사용
  • 또한 그룹(각 행)의 높이를 절대값이 아닌 컨테이너 너비의 20%가 되도록 지정해 주기 => 정사각형 형태

item에 inset을 주고 싶다면?

  • item의 inset을 설정하는 코드 한 줄만 추가해 주면 됨


2열 Grid

  • group을 선언하는 코드를 보면 count parameter을 사용하여 초기화함
  • 그룹당 2개의 item을 원하다고 명시적으로 지정(= 맨 처음에 정의된 itemSize가 재정의됨) ⇒ compositional layout이 item의 너비를 자동으로 파악하여 실행함
  • group.interItemSpacing : group 안의 item 간격 관리 - 이 코드에서는 10포인트의 고정 간격
  • section.interGroupSpacing : 각 group들 사이 간격 관리 - 이 코드에서는 10포인트의 고정 간격
  • section.ContentInsets : 섹션의 inset 관리 - 이 코드에서는 section이 하나이므로 전체 레이아웃에 적용됨

2열 Grid - interItemSpacing과 interGroupSpacing

// createLayout() 함수
let spacing = CGFloat(30)
group.interItemSpacing = .fixed(spacing)

let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = CGFloat(10)

section마다 다른 레이아웃을 가지고 싶다면? ✅

  • 3개의 section으로 구성된 레이아웃
  • 첫 번째 section: list
  • 두 번째 section: 정사각형 5열 grid
  • 세 번째 section: 직사각형 3열 grid
	UICollectionViewCompositionalLayout은 provider 클로저로 섹션마다 다양한 레이아웃을 정의

라고 앞서 언급했었음

코드가 많이 달라 보이지만, 실제로는 레이아웃을 인스턴스화하는 코드일 뿐이니 겁먹지 말자!

클로저에서는,

  • 해당 section에 원하는 레이아웃 종류를 반환함
  • sectionIndex : 어떤 section인지 알려주는 index
    ㄴ 이 뷰의 경우에는 0, 1, 2가 존재할 것
  • layoutEnvironment : 다양한 유용한 속성들을 포함하는 파라미터
  • section마다 다른 레이아웃을 가지고 싶을 때의 포인트: section마다 column 수가 달라진다는 점 ⇒ 이를 위해 SectionLayoutKind 관련 코드를 작성하였고, 이는 위에 enum으로 정의되어 있음
  • 이를 이용하여 해당 section에 몇 개의 column이 있어야 하는지를 알 수 있음

Column 값의 사용

  • horizontal group을 인스턴스화할 때 column 수를 명시적으로 전달
  • group의 높이를 결정할 때 사용되기도 함
// group의 높이를 결정할 때 사용
let groupHeight = columns == 1 ?
                NSCollectionLayoutDimension.absolute(44) :
                NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: groupHeight)

// horizontal group을 인스턴스화할 때 column 수를 명시적으로 전달
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)

화면 회전 시에도 효과적으로 적용하려면

enum SectionLayoutKind: Int, CaseIterable {
        case list, grid5, grid3
        func columnCount(for width: CGFloat) -> Int {
            let wideMode = width > 800
            switch self {
            case .grid3:
                return wideMode ? 6 : 3

            case .grid5:
                return wideMode ? 10 : 5

            case .list:
                return wideMode ? 2 : 1
            }
        }
}

.
.
.

func createLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout {
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            guard let layoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil }

            let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)

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

            let groupHeight = layoutKind == .list ?
                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
}
let columns = layoutKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
  • columnCount 이라는 함수 선언하여, layoutEnvironment (전체 컨테이너에 대한 정보가 포함된)에서 가져오는 너비 전달
enum SectionLayoutKind: Int, CaseIterable {
        case list, grid5, grid3
        func columnCount(for width: CGFloat) -> Int {
            let wideMode = width > 800
            switch self {
            case .grid3:
                return wideMode ? 6 : 3

            case .grid5:
                return wideMode ? 10 : 5

            case .list:
                return wideMode ? 2 : 1
            }
        }
}
  • width > 800이면, wideMode로 지정
  • wideMode에서는 더 많은 수의 열을 반환하여 화면 가로 전환 시 더 효과적으로 적용 가능

Supplementary Views & Decoration Views

NSCollectionLayoutSupplementaryItem

  • Badges
  • Headers
  • Footers

  • NSCollectionLayoutAnchor을 사용하여 정의된 위치에 고정될 것

ex. item badges

static let badgeElementKind = "badge-element-kind"

.
.
.

// createLayout() 함수
let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing], fractionalOffset: CGPoint(x: 0.3, y: -0.3))
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),
                                              heightDimension: .absolute(20))
let badge = NSCollectionLayoutSupplementaryItem(
            layoutSize: badgeSize,
            elementKind: ItemBadgeSupplementaryViewController.badgeElementKind,
            containerAnchor: badgeAnchor)

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
                                             heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])

ex. Headers & Footers

header가 고정 가능한 경우의 코드

let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .estimated(44)),
            elementKind: PinnedSectionHeaderFooterViewController.sectionHeaderElementKind,
            alignment: .top)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .estimated(44)),
            elementKind: PinnedSectionHeaderFooterViewController.sectionFooterElementKind,
            alignment: .bottom)
sectionHeader.pinToVisibleBounds = true
section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
  • header와 footer을 만들어준 후 section의 boundarySuplementaryItems 배열로 지정해 준다.
  • 고정을 원하지 않으면 pinToVisibleBoundsfalse

Background Decoration

  • elementKind 로 생성하고 등록해 주기만 하면 됨용

Estimated Self-Sizing

  • 왼쪽: 보통 text 사이즈/ 오른쪽: 아이폰의 text size 위젯을 사용하여 text 사이즈 크기 변경
  • height dimension은 estimated(추정됨)

  • headerSize 에서 너비는 정확히 알고 있지만, 높이는 정확히 명시하지 않고 44point로 추정함
  • 레이아웃에 대한 재정의가 일어나는 것은 모두 자동 쏘 굿!

Nested NSCollectionLayoutGroup ✅

  • 왼쪽의 큰 친구는 leadingItem
  • 오른쪽은 trailingItem 2개로 이루어져 있는 vertical trailingGroup
  • 그리고 group은 leadingItemtrailingGroup으로 이루어진 horizontal containterGroup

Nested CollectionView ✅

AppStore 같은 거 보면,,, Nested CollectionView 그 잡채

  • 전체 collectionView는 세로로 scroll됨
  • 총 5개의 section
  • 각 section은 직각으로(orthogonally) scroll됨

요 코드 하나만 추가하면 됨
뭐하는 친구인지 쫌 더 보면

  • 총 5가지 방법(2개의 continuous cases, 3개의 paging cases)
  • none: section 직각 스크롤 허용 ❌
  • continuous: 많이 쓰는 간단하고 직관적인 스크롤
  • continuousGroupLeadingBoundary: 직각 스크롤하여 보이는 group의 경계에서 자연스럽게 멈춤
  • paging: 직각으로 paging 가능
  • groupPaging: 한번에 한 group씩 직각으로 paging
  • groupPagingCentered: 한번에 한 group씩 직각으로 paging하며 해당 그룹을 중앙에 배치

얘네에다가 물론 부수적인 코드를 추가해야 하지만 베이스 코드는 요 정도인 듯하다! dataSource까지 물론 봐야 더 이해가 잘 될 듯함


WWDC 관심 있는 주제 생기면 보겠다고 했는데 어쩌다 보니 첫 번째가 요놈이 됐다 🥲
최대한 이해해 보고 싶어서 5번 넘게 본 것 같음 ,,, 영상까지는 다 이해가 됐는데 샘플 코드를 보면 아직 많이 어려운 것 같다. ㅎㅎ,,

0개의 댓글