 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/Apple Dev Documents

[WWDC종합] UICollectionView의 진화

singularis7 2023. 5. 1. 17:55
반응형

Overview

UICollectionView의 역사와 진화 과정을 두루두루 살펴본다.

역사

iOS6 때 처음으로 UICollectionView가 출시되었다. API는 다음과 같은 3가지 측면으로 분해할 수 있다.

  • (데이터) indexPath 기반 프로토콜인 UICollectionViewDataSource를 통해 관리되었다.
  • (레이아웃) 추상 클래스인 UICollectionViewLayout과 구상 클래스인 FlowLayout이 제공
  • (프레젠테이션) UICollectionViewCell 및 UICollectionReusableView라는 View 타입이 제공

iOS13부터 데이터는 Diffable Data Source 레이아웃의 경우 Compositional Layout를 통해 관리되었다.

iOS14는 이전 API를 바탕으로 Diffable Datasource, Compositional Layout 및 Cell 3가지 API 범주 모두에 새로운 기능을 도입했다.

핵심

UICollectionView를 구성하는 API는 Data Layout Presentation 세가지 범주로 나눌 수 있다. UICollectionView를 유연하게 만드는 핵심 개념은 콘텐츠가 렌더링 되는 위치인 레이아웃으로부터 데이터의 관심사를 분리하는 것이다.

UICollectionViewLayout

레이아웃을 정의하기 위한 별도의 추상화가 있다. 따라서 실제로는 두 클래스가 협력하여 동작한다. 하나는 View 렌더링을 수행하고 다른 하나는 데이터가 어디에 위치할지를 의미하는 레이아웃을 담당한다.

Flow Layout

CollectionViewLayout은 추상 클래스이기 때문에 구상 클래스가 필요하다. 처음에는 iOS6와 함께 FlowLayout이라는 구상 레이아웃 클래스를 제공했다. FlowLayout은 다양한 디자인에 사용될 수 있었는데 라인 기반 레이아웃 시스템을 사용하기 때문에 가능했다.

정리

라인 기반 레이아웃 시스템을 사용하면 활용 가능한 공간을 채우고 다음 라인을 놓을 때까지 레이아웃 축의 직교 축에 레이아웃할 수 있다.

문제점

현대적인 앱은 모든 것이 복잡하다. 장치도 다양해지고 화면 크기도 다르기 때문이다. 예를 들어 앱스토어와 같은 레이아웃을 만들기 위해 Flow Layout을 사용하는 것이 유용하지 않을 수 있다.

대안으로 Custom 레이아웃을 만들 수 있다. 다만, 만드는 과정이 좀 복잡하다. 레이아웃 클래스에게 제공해야 하는 일정량의 코드가 있고 성능 측면의 고려사항도 있다. 특히 Supplementary, decoration view를 붙이는 것도 어렵다.

Compositional Layout

FlowLayout이 갖는 문제점을 해결하고자 iOS13 때 개발된 레이아웃이 Compositional Layout이다.

철학

Compositional Layout은 다음의 3 기둥을 바탕으로 개발되었다.

Composable 하다. 단순한 것에서 복잡한 것을 만드는 아이디어이다. 작은 레이아웃 구성 요소를 묶어내어 더 큰 레이아웃을 구성하는 것이 전부이다.

Flexable 하다. Compositional Layout으로 모든 레이아웃을 작성할 수 있다. Compositional Layout을 활용해 Flow Layout을 만드는 것도 가능하다. 또한 하위 클래를 만들지 않는다.

Fast 하다. 프레임워크에서 모든 성능 최적화를 수행했기에 개발자는 성능에 대해 생각할 필요가 없다. 선언형 타입의 API이기 때문에 사용자는 수행하려는 작업을 설명하거나 정의하면 된다.

Hello World Code

// create a List by Specifying Three Core Components: Item, Group, and Section

let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                 heightDimension: .absolute(44.0))
let item = NSCollectionLayoutItem(layoutSize: size)

let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subItems: [item])

let section = NSCollectionLayoutSection(group: group)

let layout = UICollectionViewCompositionalLayout(section: section)

특징을 관찰해 본다. 우선, 레이아웃이 점차 복잡해짐에 따라 코드의 양이 문제의 크기에 선형적으로 증가하지 않고 줄어들고 있다. 복잡한 레이아웃을 개발하기 위해선 단순한 레이아웃에 새로운 것을 구성하기 때문이다.

다음으로 5가지 타입간 자연스러운 추상성을 갖고 있다. item은 group으로, group은 section으로, section은 레이아웃으로 묶이고 있다.

계층구조를 살펴본다. layout은 전체 레이아웃을 의미한다. layout 아래는 섹션이 있는데 해당 섹션의 콘텐츠인 데이터 소스에 직접 매핑된다. 전통적 스타일인 테이블처럼 생각했을 때 Group은 Row에 해당되고 내부에는 item이 있다. 앞으로 이런 패턴이 반복적으로 사용된다.

핵심 타입

NSCollectionLayoutSize

모든 것은 명시적인 사이즈를 갖고 있다. 사이즈가 얼마나 큰지를 의미하는 것이다. 사이즈는 유클리드2D 평면 위에 있기 때문에 너비와 높이를 의미한다.

이 클래스의 정의를 보면 너비와 높이 값이 Primitive 타입이 아닌 NSCollectionLayoutDimension으로 되어있다. 특정 축이 얼마큼 큰지 설명하는 축 독립적(너비, 높이)인 방법이다. 4가지 변형된 방법이 존재한다.

분수 기반 값인 다음을 사용해 너비와 높이를 차원으로 정의해 특정 Aspect Ratio를 정의할 수 있다.

  • fractionalWidth: 해당 컨테이너 너비에 대해 얼만큼의 너비를 가질 것인가? (즉, 분수 너비)
  • fractionalHeight: 해당 컨테이너 높이에 대해 얼만큼의 높이를 가질 것인가? (즉, 분수 높이)

다음은 포인트 기반 값을 의미한다.

  • absolute: 단순한 절대값
  • estimated: 항목이 얼마큼 커질지 모를 때 우리는 값을 추정할 수 있다.

NSCollectionLayoutItem

Item은 Cell 혹은 Supplementary Cell을 의미한다. 실제로 화면에 렌더링 되는 부분이다. 모든 것에는 크기를 갖고 있고 Item도 마찬가지로 크기를 갖는다. 따라서 Item 정의 코드를 보면 사이즈를 받는 파라미터를 확인할 수 있다.

NSCollectionLayoutGroup

그룹은 일종의 일꾼으로 함께 구성할 레이아웃의 기본단위이다. 세 가지 형태가 있다. 우선, 가로형과 세로형 있다. 이들은 작은 Flow Layout처럼 생각할 수 있다. 가로축과 세로축 선에 배치되기 때문이다.

선에 따라 배치되지 않는 레이아웃이 있을 수 있다. 이 경우 커스텀 레이아웃을 정의하여 유연함을 살릴 수 있다. 커스텀 레이아웃을 사용하면 사용자 지정 방식으로 아이템의 절댓값 크기와 위치를 지정할 수 있다.

NSCollectionLayoutSection

섹션별 레이아웃의 정의이며 해당 섹션에 얼마큼의 Item이 있는지에 대한 데이터 소스 개념에 직접 매핑된다. 생성자에서 그룹을 받는 것을 확인할 수 있다. 추상화 계층 구조의 일환이다.

(UI/NS) CollectionViewCompositionalLayout

우리의 최종 목적지인 최상위 레이아웃 클래스이다. iOS 및 tvOS의 경우 UIKit을 사용하고 macOS의 경우 AppKit을 사용하기에 클래스명의 접두사 차이가 발생한다. 핵심은 최상위 레이아웃 클래스를 제외한 나머지 레이아웃의 정의는 플랫폼에 의존하지 않는다는 점이다.

섹션을 정의하는 부분을 흥미롭게 볼 수 있다. 가장 간단한 방법은 LayoutSection의 정의를 지정하는 것인데, 컴포지션 레이아웃은 섹션에 대한 훌륭한 정의를 갖고 있기 때문에 콜백 클로져를 활용해 섹션마다 다른 레이아웃 섹션 정의를 사용할 수 있다.

Advanced Layout

CollectionView는 SupplementaryItem과 DecorationItem을 가질 수 있다. 이들은 콘텐츠 정보에 대한 시각적 단서를 제공하고자 레이아웃의 다른 부분을 꾸민다. 예를 들어, 특정 트윗에 댓글이 있다고 말하는 셀의 배지를 생각해 볼 수 있다. Badge, Header, Footer 세 가지 예를 통해 일반적 용도를 알 수 있다.

NSCollectionLayoutAnchor

Compositional Layout을 활용하면 SupplementaryItem 등을 더욱 쉽게 만들 수 있다. 콘텐츠를 레이아웃의 Item 혹은 Group에 고정(Anchored)할 수 있다는 개념으로 단순화할 수 있기 때문이다.

예를 들어 호스트의 geometry에 따라 상대적인 위치를 선언할 수 있다. 사각형 구조의 host가 있을 때 글 쓰는 방향 기준 꼬리 상단은 [. trailing, .top] 형태로 표현할 수 있다. 그냥 상단부는 [.top]으로 하단부는 [.bottom] 으로 표현할 수 있다. 아주 직관적이다.

NSCollectionLayoutSupplementaryItem

일반적으로 자주 사용되는 Badge는 SupplementaryItem을 정할 때 사용되는 클래스이다. 해당 아이템의 크기와 종류 그리고 컨테이너의 세부 위치를 명시하는 Anchor 정보를 통해 SupplementaryItem을 생성할 수 있다. LayoutItem을 생성할 때 supplemetaryItem을 포함시킴으로써 사용할 수 있다.

Header와 Footer의 경우 Badge와 구현상 차이점이 있다. BoundarySupplementaryItem을 사용한다는 점이다. SupplementaryItem이 콘텐츠를 가리지 않도록 콘텐츠의 영역을 확장시켜 준다. Anchor를 사용하지 않고 정렬 파라미터를 사용한다. pinToVisibleBounds를 지정해 헤더를 스크롤 시 보이는 영역에 고정시킬 수 있고 header와 footer를 포함할 geometry에 연결해야 하기 때문에 section의 boundarySupplementaryItems 프로퍼티의 값으로 [header, footer]를 설정해주어야 한다.

앱스토어에서 보이는 카드형 UI도 UICollectionDecorationItem을 통해 만들 수 있다. background를 생성해할 수 있다. section의 decorationItems 프로퍼티를 설정해 멋진 시각적 그룹화를 제공하기 위한 섹션 콘텐츠 배경 뷰를 가질 수 있다. 이를 사용하기 위해 레이아웃에 데코레이션 뷰를 등록해주어야 한다.

Estimated Self-Sizing

컴포지션 레이아웃을 사용하여 전보다 빠르게 Self-Sizing을 구현할 수 있다. 특히 축별 Self-Sizing을 구현할 수 있는데 매우 중요한 개념이다.

높이가 약간 변화하는 상황이 있을 수 있다. 예를 들어 다이내믹 폰트 시스템을 사용해 폰트 크기가 커질 경우 이를 보여주는 셀의 크기도 커져야 한다. estimate 속성을 사용해 콘텐츠가 로드되었을 때 이전 레이아웃을 무효화하고 알맞은 새로운 레이아웃을 결정할 수 있다.

Nested-Group

그룹을 중첩해 새로운 레이아웃을 구성할 수 있다. UICollectionLayoutSectionOrthogonalScrollingBehavior 코드를 갖고 직교 방향으로 스크롤되는 앱스토어와 같은 뷰를 만들 수 있다. 적정 맥락에서 스크롤이 멈추도록 설정하는 것, 페이징 기능을 설정하는 것 모두 오프셋을 설정하지 않고 손쉽게 구현할 수 있다.

장점

Compositional Layout은 모든 것을 갖고 있다. iOS, tvOS, 그리고 macOS에서 사용할 수 있으며 새로운 커스텀 레이아웃을 매우 쉽게 생성할 수 있다. 임의의 방식으로 콘텐츠를 표시할 수 있는 다재다능한 도구가 되었으며 새로운 레이아웃을 쉽게 설명하고, 레이아웃을 조정하고, 다른 변경을 하고, 다른 것을 시도하고, 반복할 수 있으므로 디자이너와 더울 빠르게 작업할 수 있다.

New Features on Compositional Layout

앞서 소개한 것처럼 API는 Data Layout Presentation 세 가지 범주로 분류할 수 있다. 초기에 Delegate 패턴으로 구현된 IndexPath기반 프로토콜은 iOS13 시기에 Diffable Data Source와 Composition Layout으로 발전되었다. iOS14부터는 발전된 최신 API를 기반으로 CollectionView API 3 범주에 들어가는 새로운 기능이 개발되었다. 데이터에는 Section Snapshot, Compositional Layout에는 List Configuration, Presentation에는 List Cell과 View Configuration이 추가되었다.

특징

애플에서 공개한 Emoji Exploler 앱을 통해 새로운 기능을 살펴본다. 첫 번째 섹션에는 가로로 스크롤이 되는 이모지 그리드가 보이는데 오늘날의 앱에서 매우 일반적으로 사용되는 설계 요소이다. 두 번째 섹션에는 확장 및 축소가 가능한 개요(outline)를 볼 수 있다. 마지막으로 UICollectionView에서 List 형태의 UITableView와 유사한 디자인이 보인다.

Diffable Data Source

새로운 Snapshot 데이터 타입을 추가해 UI의 상태 관리를 단순화시켜주는 기술이다. Snapshot은 고유한 섹션 및 아이템 식별자를 사용해 전체 UI의 상태를 캡슐화한다. UICollectionView를 업데이트하기 위해 먼저 새로운 스냅숏을 만들고 현재 UI의 상태로 채운 다음 데이터 소스에 적용하는 방식으로 동작한다.

Diffable Data Source는 상태의 차이를 계산하여 추가 작업 없이도 자동으로 애니메이션을 생성해 준다.

iOS14에서는 섹션 스냅숏이라는 새로운 타입이 추가되었다. 섹션 스냅숏은 UICollectionView의 단일 섹션에 대한 데이터를 캡슐화한다. 왜 이렇게 개선되었는가?

  1. 섹션 측면의 데이터에서 데이터 소스를 Composable 하도록 만들기 위함이다.
  2. iOS14에서 볼 수 있는 일반적인 시각적 디자인인 개요 스타일의 UI 렌더링을 지원하는데 필요한 계층적 데이터의 모델링을 위함이다.

List Compositional Layout

iOS14에서는 Compositional Layout을 기반으로 List라는 새로운 기능이 구현되었다. 리스트를 사용하면 UICollectionView에 바로 UITableView와 유사한 섹션을 포함할 수 있다. UITableView에서 기대할 수 있는 스와이프 동작 및 많은 일반적인 셀 레이아웃 기능을 리스트에서 활용할 수 있다.

리스트를 사용하는 것은 쉽다. Compositional Layout 위에서 구현되었기 때문에 List를 섹션별로 다른 종류의 레이아웃과 쉽게 혼합하여 구현할 수도 있다.

Modern Cell

iOS14의 경우 CellRegistration이라고 하는 새로운 API를 통해 셀을 구성할 수 있다. 이는 ViewModel에서 셀을 설정하는 간단하고 재사용 가능한 방법이다. Cell Registration을 사용하면 reuseIdentifier을 연결하는 등의 추가 구현을 없앨 수 있다. 대신에 새로운 셀을 설정하기 위한 클로저를 활용하는 제네릭 타입의 등록 코드를 사용한다.

Cell Content Configuration

셀 콘텐츠 구성은 UITableView 표준 셀 타입에 표시되는 것과 유사한 셀에 대해 표준화된 레이아웃을 제공한다. 이 구성은 모든 셀 혹은 일반 UIView와 함께 사용할 수 있다. 매우 유연하다.

반응형