[WWDC18] - A Tour of UICollectionView

sewoong·2023년 1월 8일
0

WWDC

목록 보기
4/6
post-thumbnail
👨‍💻 WWDC 2018 'A Tour of UICollectionView'를 읽고 정리한 글입니다.

A Tour of UICollectionView

# Introduction

UICollectionView는 애플리케이션에서 뛰어난 사용자 경험을 달성하는 데 도움이 되는 유연하고 강력한 도구입니다. UICollectionView API를 시작하기부터 고급 업데이트 애니메이션까지 다루는 방법을 알아봅시다.

# UICollectionView Concepts

오늘 다룰 것

  • 레이아웃
  • 업데이트
  • 애니메이션

Key UICollectionView Concepts

코드를 보기 전에 CollectionView에 대해 이해해야 할 세 가지 핵심 개념을 보겠다. 바로 Layout, Data Source, Delegate이다.

UICollectionViewLayout

Layout

  • 컨텐츠들의 시각적 배열.
  • UITableView와 유사
  • Attribute들: Bounds, center, frame...
  • CollectionViewLayout은 추상 클래스이므로 직접 사용하는 것이 아니라 하위 클래스를 사용한다.

UICollectionViewFlowLayout

  • UICollectionViewLayout의 Concrete 하위 클래스(이미 만들어져있는 클래스)
  • UICollectionViewDelegate를 확장한 UICollectionViewDelegateFlowLayout사용
  • line-based

Flow Layout

1. Line Orientation

이것을 따라서 항목을 배치할 수평선의 방향을 의미한다. Horizontal / Vertical 두 가지가 존재한다.

2. Line Spacing

Line Spacing은 그림에서 수평선 사이의 간격을 의미하는데, 항목들이 배치되는 수평선 사이의 공간을 의미한다.

3. Inter-Item Spacing

Inner-Item Spacing은 레이아웃 선을 따라서 생긴 항목들 사이의 공간들을 의미한다.

UICollectionViewDataSource

Data Source
UITableView를 사용할 때의 코드와 매우 유사하다. Layout이 컨텐츠를 어디에 그릴지 결정하는 것이라면, Data Source는 컨텐츠가 무엇인지에 해당한다.
여기서 핵심적인 메서드는 다음 세 가지

  • optional func numberOfSection(in collectionView: UICollectionView) -> Int: CollectionView의 섹션 수이며, 디폴트 값은 1이다.
  • func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int: 각각의 개별 섹션에 있는 항목들의 수를 나타낸다.
  • func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell: 사용자에게 표시할 실제 컨텐츠를 제공하는 메서드이다.

UICollectionViewDelegate

Delegate

  • Delegate를 사용하는 것은 부가적(optional)인 행동이다.
  • CollectionView는 UIScrollView를 상속받기 때문에 ScrollView에서 제공하는 것과 동일한 Delegate를 사용하지만 확장할 수 있다.
  • 스크롤 동작을 수정할 경우, 그리고 콘텐츠와 상호 작용할 때의 제어(강조 표시 혹은 선택에 대한 기능 제공) 등을 수정하는 경우 UICollectionViewDelegate의 메서드로 작업한다.
  • 또한 WillDisplayItem(화면에 나타났을 때), DidEndDisplayingItem(화면에 사라졌을 때)와 같은 메서드도 제공한다.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    <#code#>
}

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    <#code#>
}

Demo(코드로 살펴보기)

미리 준비한 UICollectionViewFlowLayout을 상속받은 ColumnFlowLayout을 준비했음.
이걸로 CollectionView를 생성한다.
그리고 DataSource와 Delegate를 self로 선언한다.

DataSource 관련 필수 메서드 2개를 구현

Delegate 메서드 구현. 클릭했을 때의 동작 구현을 위해 메서드 구현

ColumnFlowLayout의 코드 내용. 항목 크기를 설정하고, 간격을 보기좋게 조정했다.

시뮬레이터 실행 모습.

UICollectionViewLayoutsPrepare() 메서드는 레이아웃이 무효화될 때마다 호출되며, FlowLayout의 경우 CollectionView의 bounds가 변경될 때마다 레이아웃이 무효화된다. 예를 들면 디바이스가 회전하거나 실행하는 화면이 조정되는 경우이다.

사진은 가로로 디바이스를 회전시킨 상황인데, 회전하면서 레이아웃이 무효화되고 재설정하는 메서드가 호출되어 레이아웃을 다시 맞춰주는 모습이다. 그러나 가로로 방향을 회전하면서 더 많은 항목들을 표시할 수 있다. 이 때 FlowLayout을 사용하면 쉽게 해결할 수 있다. 앞에서 얘기했던 대로 Flow Layout은 다음 줄로 넘어가기 전에 한 줄에 가능한 한 많은 항목들을 배치하려고 하는 성질이 있다. 따라서 항목의 크기를 변경해주면 된다.

itemSize 설정 부분을 다음과 같이 바꿔주었다. 임의의 너비를 300포인트로 잡고 한 줄에 들어갈 수 있는 열 수를 계산한다. 이를 기반으로 사용 가능한 너비를 구해 itemSize에서 사용한다.

원하는 대로 결과가 출력된 모습.


When to Go Custom?

이것은 Flow Layout으로 작동하지 않을 것이다. 왜냐하면 라인 기반 레이아웃의 모습이 아니기 때문이다.

Creating a Custom UICollectionViewLayout

이런 레이아웃을 짜려면 당연하게도 커스텀 레이아웃을 만들어야 한다. 이럴 때 사용하는 유용한 4+1개의 메서드를 소개하겠다.

Providing Content Size

첫 번째 메서드는 collectionViewContentSize.
UICollectionView가 UIScrollView의 하위 클래스인 것을 기억해야 한다.
UIScrollView의 기능 중 하나는 커다란 컨텐츠 영역이 있고 그 안에서 컨텐츠가 이동한다는 것이다.
이 메서드(프로퍼티)는 모든 항목들을 포함하는 사각형 모양의 크기를 반환한다.

Providing Layout Attributes

다음 두 메서드는 레이아웃의 속성들을 제공하는 메서드이다.
먼저, layoutAttributesForElements(in rect: CGRect)는 항목을 처음 표시하거나 사용자의 스크롤로 인해서 화면에 표시해야 하는 항목을 알아야할 때 주기적으로 호출된다. 기하학적인 영역에 대한 것
다음은 layoutAttributesForItem(at indexPath: IndexPath)이다. indexPath의 단일 항목에 대한 속성을 알려주는 메서드이다.

Preparing the Layout

네 번째 메서드는 prepare().
레이아웃이 무효화될 때마다 호출된다.

Handling Bounds Changes in Your Custom Layout

CollectionView의 bounds가 변경될 때마다 호출되는 메서드.
bounds의 변경이란 앱의 크기가 변경되거나, CollectionView의 크기가 변경되거나 혹은 스크롤을 통해 원점이 변경되는 상황을 가리킨다. 즉 이 메서드는 스크롤할 때마다 호출될 것인데 기본적으로는 false를 반환한다.

Demo(코드로 살펴보기)

MosaicLayout 생성. UICollectionViewLayout의 하위 클래스
contentBounds: 모든 항목들의 대표 범위
cachedAttributes: 성능이 중요할 떄 빠르게 참조할 수 있도록 캐시된 속성 배열

prepare는 레이아웃이 무효화될 때마다 한번씩 호출되기 때문에 설정을 잡아주기 적합하다.
createAttributes 메서드에서 할 동작

  • 나중에 빠르게 가져올 수 있도록 속성 배열에 캐싱
  • contentBounds와 attributes.frame의 결합(contentBounds의 최신화)

따라서 위에서 얘기했던 메서드를 구현한다. collectionView의 bounds가 변경될 때만 true를 반환.

시뮬레이터로 확인한 레이아웃 모양. 모자이크 구성으로 멋지게 표현되었으며 회전을 통해 bounds가 변경되었기 때문에 레이아웃이 무효화되고 새로 업데이트하는 것을 볼 수 있다.
그러나 스크롤 성능은 매우 좋지 않다. 코드로 돌아가서 (스크롤할 때) 무슨 일이 일어나고 있는지 확인해보자.

이 메서드가 스크롤하는 동안 너무 자주 호출되고 있다. 전체 배열을 필터링하기 때문에 CollectionView의 항목 수가 많은 경우 비용이 많이 든다.

layoutAttributesForElements 메서드를 효율적으로 개선해보자. 먼저 (미리 준비한) 이진 탐색 메서드를 사용하여 인덱스를 찾고, 그 위치에서 시작하여 위아래로 반복문을 돌려 나머지 속성 집합을 구축하는 방법으로 구현하였다. 간단하게 설명하면, 배열을 반복하지 않기 때문에 메서드 호출에 대한 비용이 줄어들어 스크롤이 빨라졌다.


Our Totally Cool Update Animation

이런 애니메이션
UICollectionView가 제공하는 도구인 Perform Batch Updates API를 사용할 것임.
이 API를 사용하면 애니메이션과 동시에 수행하는 일련의 업데이트를 Collection View에 전달할 수 있다.

PerformBatchUpdates에 대한 호출을 추가. Data Source 업데이트와 Collection View 업데이트를 모두 수행하고 있는데 이는 동기화를 깔끔하게 유지하고 불일치를 피하는 좋은 방법이다.

에러 발생!

performBatchUpdates()

동시에 여러 업데이트를 진행하고 애니메이션 제공
Data Source 업데이트와 동시에 CollectionView 업데이트 진행
Data Source 업데이트의 순서는 중요하지만 CollectionView 업데이트는 순서가 중요하지 않다.

Data Source Updates: Order Matters

3개의 요소가 있는 2개의 배열로 예시를 들 것이다.

우리의 직관대로라면 순서에 의해 결과가 다르게 나타날 것이다. 이는 우리가 원하는 결과가 아니다.

그런데 코드와 같이 클로저로 업데이트 동작을 지정하고 performBatchUpdates()를 수행하면 결과가 동일하게 나올 것이다.

Collection View Updates Coalescing

CollectionView로 전송되는 업데이트의 순서가 중요하지 않은 이유는 무엇일까?
업데이트 작업들을 각각 살펴보겠다.
1. Delete

  • IndexPath 내림차순으로 처리된다.
  • 항상 이전 상태의 indexPath를 참조한다.
    (Data Source 업데이트 이전과 이후 상태를 각각 이전/이후 상태라고 칭함)
  1. Insert
  • IndexPath 오름차순으로 처리된다.
  • 삽입 작업에서 참조하는 IndexPath는 항상 최종 상태 혹은 업데이트 이후 상태르 참조한다.
  1. Move
  • Delete와 Insert의 혼합 작업
  • From은 이전 상태, to는 이후 상태
  1. Reload
  • 실제로 삽입과 삭제로 분리할 수 있다.
  • reload에서 지정된 IndexPath는 이전 상태를 참조한다.

따라서 다음과 같은 작업들의 결합이 일어나게 되면, Collection View는 충돌이 일어날 것이다.

  • 같은 위치에 이동과 삭제를 시도할 때
  • 같은 위치에 이동과 삽입을 시도할 때
  • 둘 이상의 위치를 동일한 위치로 이동을 시도할 때
  • 유효하지 않은 IndexPath를 참조할 때

오류를 범하지 않으려면 이 네 가지 규칙을 지키면서 항상 Data Source 업데이트를 적용하고 동기화되었는지 확인해야 한다.

  • Move와 Delete 및 Insert를 분리한다.
  • 모든 삭제 작업과 삽입 작업을 두 개의 개별 목록으로 분리한다.
  • 먼저 내림차순으로 삭제를 처리한 다음 오름차순으로 삽입을 적용한다.

What About reloadData()?

  • 위의 업데이트 작업 없이 Data Source와 Collection View의 동기화 작업을 하는 메서드
  • 애니메이션이 제공되지 않음.
  • "The Sledgehammer"
  • 애니메이션이 없기 때문에 사용자에게 좋은 경험을 주지 않으므로 꼭 필요한 경우에만 사용해야 함.

Demo(코드로 살펴보기)

같은 IndexPath를 사용하는 것에서 충돌이 시작함.
따라서 이를 분리하는 작업을 먼저 시작하겠다.

Remove와 Move, Insert를 각각 분리한다.
먼저 내림차순으로 삭제를 수행한다.
그 후 삽입을 오름차순으로 처리한다.
마지막으로 CollectionView를 업데이트하여 애니메이션을 재생한다.

profile
iOS Developer

0개의 댓글