1. 1. iOS를 위한 컬렉션 뷰 프로그래밍 가이드
    1. 1.1. iOS의 컬렉션 뷰에 대하여
      1. 1.1.1. 훑어보기
        1. 1.1.1.1. 컬렉션 뷰는 데이터 기반 뷰의 시각적 표현을 관리한다
        2. 1.1.1.2. 플로우 레이아웃은 격자 모양 및 다른 라인 지향 표현을 지원한다
        3. 1.1.1.3. 제스처 레코그나이저를 사용하여 셀과 레이아웃을 조절할 수 있다
        4. 1.1.1.4. 커스텀 레이아웃은 격자 모양 이상의 것을 할 수 있게 해준다
      2. 1.1.2. 전제조건
      3. 1.1.3. 함께 보기
    2. 1.2. 컬렉션 뷰 기본
      1. 1.2.1. 컬렉션 뷰는 객체 간 협력이다
      2. 1.2.2. 재사용 가능한 뷰는 성능을 향상시킨다
      3. 1.2.3. 레이아웃 객체는 시각적 표현을 제어한다
      4. 1.2.4. 컬렉션 뷰는 자동으로 애니메이션을 개시한다
    3. 1.3. 데이터 소스 및 델리게이트 설계하기
      1. 1.3.1. 데이터 소스는 컨텐츠를 관리한다
      2. 1.3.2. 데이터 객체 설계하기
      3. 1.3.3. 컬렉션 뷰에게 컨텐츠에 대해 알리기
      4. 1.3.4. 셀 및 서플먼터리 뷰 구성하기
        1. 1.3.4.1. 셀 및 서플먼터리 뷰 등록하기
        2. 1.3.4.2. 셀 및 뷰를 디큐하고 구성하기
      5. 1.3.5. 섹션 및 아이템을 삽입, 삭제, 이동하기
      6. 1.3.6. 선택 및 하이라이팅에 대한 시각적 상태 관리하기
      7. 1.3.7. 셀에 대하여 편집 메뉴 표시하기
      8. 1.3.8. 레이아웃 간 전환하기
    4. 1.4. 플로우 레이아웃 사용하기
      1. 1.4.1. 플로우 레이아웃 애트리뷰트 커스터마이징하기
        1. 1.4.1.1. 플로우 레이아웃 내의 아이템들의 크기 지정하기
        2. 1.4.1.2. 아이템 및 라인 간 간격 지정하기
        3. 1.4.1.3. 컨텐츠의 마진을 비틀기 위해 섹션 인셋 사용하기
      2. 1.4.2. 플로우 레이아웃을 서브클래싱해야 할 때를 알기
    5. 1.5. 제스처 지원하기
      1. 1.5.1. 레이아웃 정보를 변경하기 위해 제스처 레코그나이저 사용하기
      2. 1.5.2. 기본 제스처 동작으로 작업하기
      3. 1.5.3. 셀 및 뷰 조작하기
    6. 1.6. 커스텀 레이아웃 만들기
      1. 1.6.1. UICollectionViewLayout 서브클래싱하기
        1. 1.6.1.1. 핵심 레이아웃 프로세스 이해하기
        2. 1.6.1.2. 레이아웃 애트리뷰트 만들기
        3. 1.6.1.3. 레이아웃 준비하기
        4. 1.6.1.4. 주어진 직사각형 내의 아이템에 대한 레이아웃 애트리뷰트 제공하기
        5. 1.6.1.5. 필요할 때 레이아웃 애트리뷰트 제공하기
        6. 1.6.1.6. 커스텀 레이아웃을 사용하기 위해 연결하기
      2. 1.6.2. 커스텀 레이아웃을 더욱 매력적이게 만들기
        1. 1.6.2.1. 서플먼터리 뷰를 통해 컨텐츠를 고양시키기
        2. 1.6.2.2. 커스텀 레이아웃에 데코레이션 뷰 포함시키기
        3. 1.6.2.3. 삽입 및 삭제 애니메이션을 더욱 흥미롭게 만들기
        4. 1.6.2.4. 레이아웃의 스크롤 경험을 향상시키기
        5. 1.6.2.5. 커스텀 레이아웃을 구현하기 위한 팁

[번역] Collection View Programming Guide for iOS

iOS를 위한 컬렉션 뷰 프로그래밍 가이드

iOS의 컬렉션 뷰에 대하여

컬렉션 뷰는 유연하고 변경 가능한 레이아웃을 사용하여 순서 있는 데이터 아이템들의 집합을 나타내는 방법이다. 컬렉션 뷰를 사용하는 가장 흔한 경우는 격자 모양의 정렬에서 아이템을 나타내기 위한 것이지만, iOS에서 컬렉션 뷰는 단지 행과 열 이상의 능력을 가지고 있다. 컬렉션 뷰를 사용하여 시각적 요소의 정확한 레이아웃은 서브클래싱을 통하여 정의 가능하고 동적으로 변화할 수 있으므로, 격자, 스택, 원형 레이아웃, 동적으로 변화하는 레이아웃, 또는 상상할 수 있는 어떠한 형태의 정렬이라도 구현할 수 있다.

컬렉션 뷰는 나타나질 데이터와 그 데이터를 나타내기 위해 사용되는 시각적 요소를 명확하게 구분한다. 대부분의 경우에 앱은 데이터를 관리할 책임이 있다. 또한 그 데이터를 나타내기 위해 사용되는 뷰 객체를 제공한다. 이후에 컬렉션 뷰는 뷰들을 가지고 화면에 배치하는 모든 작업을 한다. 그것은 레이아웃 객체와 결합하여 이 작업을 하며, 레이아웃 객체는 뷰의 배치와 시각적 애트리뷰트들을 지정하고 앱의 정확한 니즈를 충족시키기 위해 서브클래싱될 수 있다. 그러므로 당신은 데이터를 제공하고, 레이아웃 객체는 배치 정보를 제공하고, 컬렉션 뷰는 두 조각을 결합하여 최종의 모습을 만들어낸다.

훑어보기

기본 iOS 컬렉션 뷰 클래스들은 단순한 격자 모양을 구현하기 위해 필요로 하는 모든 동작을 제공한다. 또한 기본 클래스를 확장하여 커스텀 레이아웃을 만들고 그러한 레이아웃에 특정한 인터랙션을 구현할 수 있다.

컬렉션 뷰는 데이터 기반 뷰의 시각적 표현을 관리한다

컬렉션 뷰는 앱이 제공한 데이터에 따른 뷰를 쉽게 나타낸다. 컬렉션 뷰는 오직 뷰를 가지고 특정 방식으로 배치하는 것에만 관심이 있다. 컬렉션 뷰는 그 컨텐츠가 아닌, 뷰들의 표현과 정렬에 관한 것이다. 컬렉션 뷰, 그것의 데이터 소스, 레이아웃 객체, 그리고 커스텀 객체 간 인터랙션을 이해하는 것이 특히 영리하고 능률적인 방식으로 컬렉션 뷰를 사용하는 것에서 중요하다.

플로우 레이아웃은 격자 모양 및 다른 라인 지향 표현을 지원한다

플로우 레이아웃 객체는 UIKit이 제공하는 구체적인 레이아웃 객체다. 일반적으로 플로우 레이아웃 객체를 사용하여 격자 모양, 즉 아이템의 행과 열을 구현한다. 하지만 플로우 레이아웃은 선형적인 흐름의 종류만을 지원한다. 단지 격자 모양만을 위한 것은 아니기 때문에 서브클래싱을 하든 하지 않든 컨텐츠를 흥미롭고 유연하게 정렬하기 위해 플로우 레이아웃을 만들어 사용할 수 있다. 플로우 레이아웃은 다른 크기, 아이템들의 다양한 간격, 커스텀 헤더 및 푸터, 커스텀 마진을 서브클래싱 없이도 제공한다. 플로우 레리아웃 클래스를 서브클래싱하여 그 동작을 더욱 비틀 수 있다.

제스처 레코그나이저를 사용하여 셀과 레이아웃을 조절할 수 있다

다른 모든 뷰들처럼, 컬렉션 뷰에 제스처 레코그나이저를 붙여 뷰의 컨텐츠를 조작할 수 있다. 컬렉션 뷰는 여러 개의 뷰가 함께 동작하기 때문에, 컬렉션 뷰의 제스처 레코그나이저를 통합하기 위한 기본 테크닉을 이해하는 것을 돕는다. 레이아웃 애트리뷰트를 비틀거나 컬렉션 뷰에 있는 아이템을 조작하기 위해 제스처 레코그나이저를 사용할 수 있다.

커스텀 레이아웃은 격자 모양 이상의 것을 할 수 있게 해준다

커스텀 레이아웃을 구현하기 위해 기본 레이아웃 오브젝트를 서브클래싱할 수 있다. 커스텀 레이아웃을 설계하는 것은 일반적으로 많은 양의 코드를 요구하지는 않지만, 레이아웃이 어떻게 작동하는지 더 많이 안다면 더욱 능률적으로 레이아웃 오브젝트를 설계할 수 있을 것이다. 이 가이드의 마지막 챕터는 커스텀 레이아웃의 완전한 구현이 있는 예제 프로젝트에 집중한다.

전제조건

이 문서를 읽기 전에 iOS 앱에서 뷰의 역할을 잘 이해하고 있어야 한다. iOS 프로그래밍을 처음 해보거나 iOS의 뷰 아키텍쳐에 익숙하지 않다면 이를 읽기 전 View Programming Guide for iOS를 먼저 읽어라.

함께 보기

컬렉션 뷰는 테이블 뷰와 어느 정도 관계가 있다. 둘 모두 사용자에게 순서 있는 데이터를 나타낸다. 테이블 뷰의 구현은 (제공되는 플로우 레이아웃을 사용하는) 기본적인 컬렉션 뷰의 구현과 닮아 있다. 인덱스 패스, 셀, 뷰 재활용을 사용한다. 그러나 테이블 뷰의 시각적 표현은 단일 열 레이아웃에 알맞게 설계된 반면, 컬렉션 뷰는 많은 다른 레이아웃도 지원할 수 있다. Table View Programming Guide for iOS에서 테이블 뷰에 대한 더 많은 정보를 확인하라.

컬렉션 뷰 기본

컨텐츠를 화면에 나타내기 위해 컬렉션 뷰는 많은 다른 객체들과 협력한다. 몇몇 객체는 커스터마이징되며 반드시 제공되어야 한다. 예를 들어 컬렉션 뷰가 얼마나 많은 아이템을 표시해야 하는지 알리기 위해 데이터 소스 객체를 제공해야 한다. 다른 객체들은 UIKit이 제공하며 기본적인 컬렉션 뷰 디자인의 일부분이 된다.

테이블처럼 컬렉션 뷰는 데이터 지향 객체이며, 그 구현은 앱의 객체와의 협력을 포함한다. 코드에서 무엇을 해야 하는지 이해하는 것은 컬렉션 뷰가 하는 것에 대한 약간의 배경 지식을 필요로 한다.

컬렉션 뷰는 객체 간 협력이다

컬렉션 뷰의 설계는 보여질 데이터를 그것이 화면에 정렬되고 나타나는 방법과 분리한다. 보여질 데이터를 관리할 책임이 있더라도 그 시각적 표현은 많은 다른 객체가 관리한다. 다음의 표는 UIKit에 있는 컬렉션 뷰 클래스들을 나타내고 컬렉션 뷰 인터페이스를 구현하는 것에서 그것들이 수행하는 역할에 따라 구분되었다. 대부분의 클래스들은 서브클래싱할 필요 없이 있는 그대로 사용되기 위해 설계되었다. 그러므로 보통 매우 적은 코드를 작성하여 컬렉션 뷰를 구현할 수 있다. 그리고 제공된 동작 이상의 것을 원한다면 서브클래싱하여 그 동작을 제공할 수 있다.

목적 클래스 및 프로토콜 설명
최상위 보관 및 관리 UICollectionView
UICollectionViewController
UICollectionView 객체는 컬렉션 뷰의 컨텐츠를 위한 보여질 수 있는 영역을 정의한다. 이 클래스는 UIScrollView의 자식이며 필요한 만큼 큰 스크롤 가능한 영역을 포함할 수 있다. 또한 레이아웃 객체에서 받은 레이아웃 정보에 기반한 데이터의 표현을 촉진한다.
UICollectionViewController 객체는 컬렉션 뷰에 대하여 뷰 컨트롤러 수준의 관리를 지원한다. 이것의 사용은 선택적이다.
컨텐츠 관리 UICollectionViewDataSource 프로토콜
UICollectionViewDelegate 프로토콜
데이터 소스 객체는 컬렉션 뷰와 연관되는 가장 중요한 객체이며 반드시 제공해야 하는 것이다. 데이터 소스는 컬렉션 뷰의 컨텐츠를 관리하고 그 컨텐츠를 나타내기 위해 필요한 뷰를 만든다. 데이터 소스 객체를 구현하기 위해 UICollectionViewDataSource 프로토콜을 준수하는 객체를 반드시 만들어야 한다.
컬렉션 뷰의 델리게이트 객체는 컬렉션 뷰로부터 관심 있는 메세지를 가로채고, 뷰의 동작을 커스터마이징할 수 있게 한다. 예를 들어 델리게이트 객체를 사용하여 컬렉션 뷰의 아이템의 선택과 하이라이팅을 추적할 수 있다. 데이터 소스 객체와는 다르게 델리게이트 객체는 선택적이다. Designing Your Data Source and Delegate에서 데이터 소스 객체와 델리게이트 객체를 구현하는 방법에 대한 정보를 확인하라.
표현 UICollectionViewReusableView
UICollectionViewCell
컬렉션 뷰에 표시되는 모든 뷰는 UICollectionViewReusableView 클래스의 인스턴스여야 한다. 이 클래스는 컬렉션 뷰가 사용하는 재활용 매커니즘을 지원한다. 새로운 뷰를 만드는 대신 재활용하는 것은 일반적으로 성능을 향상시키며 특히 스크롤하는 동안의 성능을 향상시킨다.
UICollectionViewCell 객체는 메인 데이터 아이템을 위해 사용하는 재사용 가능한 뷰의 특정 타입이다.
레이아웃 UICollectionViewLayout
UICollectionViewLayoutAttributes
UICollectionViewUpdateItem
UICollectionViewLayout의 서브클래스들은 레이아웃 객체로 언급되며 셀의 위치, 크기, 시각적 애트리뷰트와 컬렉션 뷰 내의 재사용 가능한 뷰를 정의하는 책임을 갖는다.
레이아웃 프로세스가 진행되는 동안 레이아웃 객체는 레이아웃 애트리뷰트 객체 (UICollectionViewLayoutAttributes의 인스턴스) 를 만든다. 이는 컬렉션 뷰에게 셀과 재사용 가능한 뷰가 어디에, 어떻게 표시되는지를 알린다.
레이아웃 객체는 데이터 아이템이 컬렉션 뷰 내에서 삽입, 삭제, 이동될 때마다 UICollectionViewUpdateItem 클래스의 인스턴스를 받는다. 이 클래스의 인스턴스를 직접 생성할 필요는 절대 없다.
The Layout Object Controls the Visual Presentation에서 레이아웃 객체에 대한 더 많은 정보를 확인하라.
플로우 레이아웃 UICollectionViewFlowLayout
UICollectionViewDelegateFlowLayout 프로토콜
UICollectionViewFlowLayout 클래스는 격자 또는 다른 선 기반 레이아웃을 구현하기 위해 사용하는 구체적인 레이아웃 객체다. 이 클래스를 있는 그대로 사용하거나, 플로우 델리게이트 객체와 결합하여 사용할 수 있다. 이는 동적으로 레이아웃 정보를 커스터마이징할 수 있도록 해준다.

컬렉션 뷰는 데이터 소스에서 표시할 셀에 대한 정보를 얻는다. 데이터 소스 객체와 델리게이트 객체는 앱이 제공하는 커스텀 객체이며, 셀의 선택 및 하이라이팅을 포함하여 컨텐츠를 관리하기 위해 사용된다. 레이아웃 객체는 그러한 셀들이 어디에 속하는지 결정하고 하나 또는 그 이상의 레이아웃 애트리뷰트 객체의 형태로 컬렉션 뷰에게 그 정보를 전달할 책임이 있다. 컬렉션 뷰는 실제 셀과 다른 뷰의 레이아웃 정보를 결합하여 최종의 시각적 표현을 만들어낸다.

컬렉션 뷰 인터페이스를 만들 때 먼저 스토리보드나 xib 파일에 UICollectionView 객체를 추가하라. 컬렉션 뷰를 중앙 허브로 생각하여, 모든 다른 객체가 그것에서 나온다고 생각하라. 그 객체를 추가한 후, 데이터 소스나 델리게이트와 같은 관련 있는 객체를 구성하는 것을 시작할 수 있다. 모든 구성은 컬렉션 뷰 그 자체 주변에서 중앙화된다. 예를 들어 컬렉션 뷰 객체를 만들지 않고 레이아웃 객체를 만들 수 없다.

재사용 가능한 뷰는 성능을 향상시킨다

컬렉션 뷰는 효율을 향상시키기 위해 뷰 재활용 프로그램을 사용한다. 뷰가 화면 밖으로 이동하면 뷰는 제거되고, 삭제되는 대신 재사용 큐에 위치하게 된다. 새로운 컨텐츠가 화면에 스크롤되면 뷰는 큐에서 제거되고 새로운 컨텐츠와 함께 다시 적용된다. 이 재활용 및 재사용을 촉진하기 위해 컬렉션 뷰가 표시하는 모든 뷰는 반드시 UICollectionReusableView 클래스로부터 상속받아야 한다.

컬렉션 뷰는 세 개의 재사용 뷰의 개별 타입을 지원한다. 각각은 의도된 특정 사용 방법을 갖는다.

  • 은 컬렉션 뷰의 메인 컨텐츠를 나타낸다. 셀의 역할은 데이터 소스 객체로부터 단일 아이템을 위한 컨텐츠를 나타내는 것이다. 각각의 셀은 반드시 UICollectionViewCell 클래스의 인스턴스여야 하며, 필요하다면 컨텐츠를 나타내기 위해 서브클래싱할 수 있을 것이다. 셀 객체는 선택 및 하이라이팅 상태를 관리하기 위한 고유의 지원을 제공한다. 실제로 셀에 하이라이팅을 적용하려면 반드시 커스텀 코드를 작성해야 한다. Managing the Visual State for Selections and Highlights에서 셀 하이라이팅 및 선택 구현에 대한 정보를 확인하라.
  • 서플먼터리 뷰는 섹션에 관한 정보를 표시한다. 셀처럼 서플먼터리 뷰도 데이터 기반으로 동작한다. 셀과는 다르게 서플먼터리 뷰는 필수가 아니며, 그것의 사용과 배치는 사용되는 레이아웃 객체가 관리한다. 예를 들어 플로우 레이아웃은 선택적인 서플먼터리 뷰로 헤더와 푸터를 지원한다.
  • 데코레이션 뷰는 레이아웃 객체가 완전히 소유하는 시각적 장식이며 데이터 소스 객체의 어떠한 데이터에도 묶여 있지 않다. 예를 들어 레이아웃 객체는 커스터마이징한 배경의 모습을 구현하기 위해 데코레이션 뷰를 사용할 수 있을 것이다.

테이블 뷰와는 다르게, 컬렉션 뷰는 데이터 소스가 제공하는 셀과 서플먼터리 뷰에 대하여 어떠한 특정 스타일도 강요하지 않는다. 대신 기본 재사용 가능한 뷰 클래스들은 당신이 수정해야 하는 빈 캔버스다. 예를 들어 작은 뷰 계층 구조를 만들기 위해, 이미지를 표시하기 위해, 심지어 동적으로 컨텐츠를 그리기 위해 그것들을 사용할 수 있다.

데이터 소스 객체는 연관된 컬렉션 뷰가 사용하는 셀과 서플먼터리 뷰를 제공해야 하는 책임이 있다. 그러나 데이터 소스는 절대 뷰를 직접적으로 만들지 않는다. 뷰가 요청할 때 데이터 소스는 컬렉션 뷰의 메소드를 사용하여 바람직한 타입의 뷰를 디큐한다. 디큐 프로세스는 항상 유효한 뷰를 반환하는데, 재사용 큐에서 되돌려받거나 새로운 뷰를 만들기 위해 제공한 클래스, xib 파일, 또는 스토리보드를 사용한다.

Configuring Cells and Supplementary Views에서 데이터 소스로부터 뷰를 만들고 구성하는 방법에 대한 정보를 확인하라.

레이아웃 객체는 시각적 표현을 제어한다

레이아웃 객체는 단독으로 컬렉션 뷰 내에서 아이템의 배치와 시각적 스타일링을 결정할 책임이 있다. 데이터 소스 객체가 뷰와 실제 컨텐츠를 제공할지라도, 레이아웃 객체는 그러한 뷰들의 크기, 위치, 겉모습과 관련된 다른 애트리뷰트를 결정한다. 이러한 책임 분리는 앱이 관리하는 데이터 객체의 변화 없이 레이아웃을 동적으로 변경할 수 있게 한다.

컬렉션 뷰가 사용하는 레이아웃 프로세스는 앱의 나머지 뷰가 사용하는 레이아웃 프로세스와 관련 있지만 구분된다. 즉 레이아웃 객체가 부모 뷰 내에서 자식 뷰의 위치를 재설정하기 위해 사용되는 layoutSubviews 메소드가 하는 일을 혼동하지 마라. 레이아웃 객체는 그것이 관리하는 뷰를 절대 직접적으로 건드리지 않는다. 실제로 그러한 뷰들 중 어느 것도 소유하고 있지 않기 때문이다. 대신 컬렉션 뷰의 셀, 서플먼터리 뷰, 데코레이션 뷰의 위치, 크기, 시각적 외관을 묘사하는 애트리뷰트를 생성한다. 그러한 애트리뷰트들을 실제 뷰 객체에 적용하는 것은 컬렉션 뷰의 역할이다.

레이아웃 객체가 컬렉션 뷰에 있는 뷰들에 영향을 미칠 수 있는 방법에는 제한이 없다. 레이아웃 객체는 뷰를 이동시킬 수 있지만 다른 것들은 안된다. 오직 약간만 뷰를 이동시킬 수 있거나, 화면 주위에서 임의로 이동시킬 수 있다. 심지어 주위의 뷰에 상관 없이 뷰의 위치를 다시 설정할 수 있다. 예를 들어 레이아웃 객체는 원한다면 서로의 상단에 뷰를 쌓을 수 있다. 유일한 제한은 레이아웃 객체가 원하는 시각적 스타일에 어떻게 영향을 미치는지에 대한 것이다.

수직 방향으로 스크롤되는 플로우 레이아웃에서 컨텐츠 영역의 너비는 고정되고 높이는 컨텐츠에 적합해지기 위해 늘어난다. 영역을 계산하기 위해 레이아웃 객체는 뷰와 셀을 차례대로 배치하고 서로에게 가장 알맞는 위치를 선택한다. 플로우 레이아웃의 경우에 셀과 서플먼터리 뷰의 크기는 애트리뷰트에 의해 지정되는데, 이는 레이아웃 객체나 델리게이트를 사용하여 지정된다. 레이아웃을 계산하는 것은 각각의 뷰를 배치하기 위해 그러한 애트리뷰트들을 사용하는 것에 대한 문제다.

레이아웃 객체는 뷰들의 크기와 위치뿐만 아니라 그 이상의 것들을 제어한다. 레이아웃 객체는 투명도, 3차원 공간에서의 변형, 다른 뷰의 위 또는 아래에서의 가시성과 같은 뷰와 관련된 다른 애트리뷰트들을 지정할 수 있다. 이러한 애트리뷰트들은 더욱 흥미로운 레이아웃을 만들 수 있게 해준다. 예를 들어 뷰를 또 다른 뷰의 위에 배치하고 그들의 z축 순서를 변경하여 셀의 스택을 만들 수 있을 것이며, 어떠한 축으로의 회전을 통한 변형을 사용할 수 있을 것이다.

Creating Custom Layouts에서 레이아웃 객체가 컬렉션 뷰에서 그 책임을 어떻게 충족시키는지에 대한 더 자세한 정보를 확인하라.

컬렉션 뷰는 자동으로 애니메이션을 개시한다

컬렉션 뷰는 기초 수준에서 애니메이션을 지원한다. 아이템이나 섹션을 삽입하거나 삭제할 때 컬렉션 뷰는 자동으로 변화에 영향을 주는 뷰들에 애니메이션 효과를 준다. 예를 들어 아이템을 삽입할 때 삽입 포인트 이후의 아이템들은 보통 새로운 아이템을 위한 자리를 만들기 위해 이동된다. 컬렉션 뷰는 아이템들의 현재 위치를 감지하고 삽입이 일어난 후 그것들의 최종 위치를 계산할 수 있기 때문에 이러한 애니메이션을 만들 수 있다. 그러므로 각각의 아이템은 처음 위치에서 최종 위치로 애니메이션과 함께 이동될 수 있다.

삽입, 삭제, 이동 작업에서 애니메이션 효과를 주는 것뿐만 아니라, 언제든 레이아웃을 무효화할 수 있고 레이아웃 애트리뷰트들을 다시 계산하도록 강제할 수 있다. 레이아웃을 무효화하는 것은 직접적으로 아이템들에 애니메이션 효과를 주지 않는다. 레이아웃을 무효화할 때 컬렉션 뷰는 애니메이션 없이 새롭게 계산된 위치에 아이템들을 표시한다. 대신 커스텀 레이아웃에서 일정 간격으로 셀을 배치하고 애니메이션 효과를 만들기 위해 이 동작을 사용할 수 있을 것이다.

데이터 소스 및 델리게이트 설계하기

모든 컬렉션 뷰는 반드시 데이터 소스 객체를 가져야 한다. 데이터 소스 객체는 앱이 표시하는 컨텐츠다. 앱의 데이터 모델로부터의 객체일 수도 있고, 컬렉션 뷰를 관리하는 뷰 컨트롤러일수도 있다. 데이터 소스의 유일한 필수조건은 이것이 컬렉션 뷰가 필요로 하는 정보를 반드시 제공할 수 있어야 한다는 것이며, 얼마나 많은 아이템이 있고 그러한 아이템을 표시하기 위해 어떠한 뷰를 사용해야 하는지와 같은 것들을 말한다.

델리게이트 객체는 컨텐츠의 표현과 컨텐츠와의 상호 작용과 관련된 양상을 관리하는 선택적(그러나 추천됨)인 객체다. 델리게이트의 주요 역할이 셀의 하이라이팅과 선택을 관리하는 것일지라도, 추가 정보를 제공하기 위해 확장될 수 있다. 예를 들어 플로우 레이아웃은 기존 델리게이트 동작을 확장하여 셀의 크기와 그것들 간의 간격과 같은 레이아웃 수치를 커스터마이징한다.

데이터 소스는 컨텐츠를 관리한다

데이터 소스 객체는 컬렉션 뷰를 사용하여 표현할 컨텐츠를 관리할 책임이 있는 객체다. 데이터 소스 객체는 반드시 UICollectionViewDataSource 프로토콜을 준수해야 하며, 이 프로토콜은 반드시 지원해야 하는 기본 동작과 메소드를 정의한다. 데이터 소스의 역할은 컬렉션 뷰에게 다음의 질문에 대한 답을 제공하는 것이다.

  • 컬렉션 뷰는 얼마나 많은 섹션을 가지고 있어야 하는가?
  • 주어진 섹션에 대하여, 섹션은 얼마나 많은 아이템을 가지고 있어야 하는가?
  • 주어진 섹션이나 아이템에 대하여, 부합하는 컨텐츠를 표시하기 위해 어떠한 뷰를 사용해야 하는가?

섹션아이템은 컬렉션 뷰 컨텐츠에 대한 기초적인 조직 원리다. 컬렉션 뷰는 일반적으로 적어도 하나의 섹션을 갖고 그 이상도 가질 수 있다. 순서대로 각각의 섹션은 0개 이상의 아이템을 갖는다. 아이템은 나타내기 원하는 메인 컨텐츠를 표현한다. 반면에 섹션은 그러한 아이템을 논리적인 그룹 안에 묶는다. 예를 들어 사진 앱은 사진으로 이루어진 하나의 앨범이나 같은 날에 촬영한 사진의 집합을 표현하기 위해 섹션을 사용할 수 있을 것이다.

컬렉션 뷰는 IndexPath 객체를 사용하여 그것이 가지고 있는 데이터를 참조한다. 아이템의 위치를 알아내려고 할 때 컬렉션 뷰는 레이아웃 객체가 제공한 인덱스 패스 정보를 사용한다. 아이템에 대하여 인덱스 패스는 섹션 숫자와 아이템 숫자를 갖는다. 서플먼터리 뷰와 데코레이션 뷰에 대하여 인덱스 패스는 레이아웃 객체가 제공한 값들을 갖는다. 서플먼터리 뷰와 데코레이션 뷰에 붙은 인덱스 패스의 의미는 앱에게 달려 있는데, 그럼에도 불구하고 첫 번째 인덱스는 데이터 소스의 특정 섹션과 부합한다. 이러한 뷰들의 인덱스 패스들은 의미보다는 식별에 관한 것이며, 어떠한 종류의 뷰가 현재 고려되고 있는지 식별하는 것이다. 그러므로 예를 들어 플로우 레이아웃에서 확인할 수 있듯 섹션에 대하여 헤더와 푸터를 만드는 서플먼터리 뷰를 가지고 있다면, 인덱스 패스가 제공하는 관련 정보는 참조하는 섹션을 말한다.

알아두기 : 표준 인덱스 패스가 여러 수준을 지원할지라도, 컬렉션 뷰의 셀들은 오직 “섹션”과 “아이템” 매개변수를 사용하여 두 단계의 깊이를 갖는 인덱스 패스만을 지원한다. 이는 UITableView 클래스를 위한 인덱스 패스와 많이 닮아 있다. 서플먼터리 뷰와 데코레이션 뷰는 필요하다면 더 복잡한 인덱스 패스를 가질 수 있다. 인덱스 패스가 1보다 큰 요소들은 패스에서 첫 번째 인덱스로 지정되는 섹션과 부합하는 것으로 해석된다. 일반적으로 오직 두 번째 인덱스만 필수적이지만 서플먼터리 뷰와 데코레이션 뷰는 오직 두 개로 제한되지 않는다. 데이터 소스를 설계할 때 이를 염두에 두어라.

데이터 객체에서 섹션과 아이템을 어떻게 배열하는지는 중요하지 않다. 그러한 섹션과 아이템의 시각적 표현은 여전히 레이아웃 객체가 결정한다. 다른 레이아웃 객체는 매우 다르게 섹션 데이터와 아이템 데이터를 나타낼 수 있다. 그림의 플로우 레이아웃 객체는 수직 방향으로 섹션을 배열하고 연속적인 각 섹션은 이전의 것 아래에 위치한다. 커스텀 레이아웃은 섹션을 비선형 정렬에 배치할 수 있으며, 다시 한번 실제 데이터와 레이아웃의 분리를 증명해낸다.

데이터 객체 설계하기

능률적인 데이터 소스는 그것의 기반 데이터 객체를 구성하는 것을 돕기 위해 섹션과 아이템을 사용한다. 데이터를 섹션과 아이템에 구성하는 것은 나중에 데이터 소스 메소드를 구현하는 것을 훨씬 더 쉽게 해준다. 그리고 데이터 소스 메소드는 주기적으로 호출되기 때문에 그러한 메소드의 구현이 가능한 한 빠르게 데이터를 가져올 수 있게 하기를 원한다.

한 가지 간단한 솔루션은 데이터 소스가 중첩된 배열의 집합을 사용하도록 하는 것이다. 이러한 구성에서 최상위 수준의 배열은 데이터 소스의 섹션들을 나타내는 하나 이상의 배열을 포함한다. 각각의 섹션 배열은 그 섹션에 대한 데이터 아이템들을 갖는다. 섹션에서 아이템을 찾는 것은 섹션 배열을 찾고 그 배열에서 아이템을 찾는 문제가 된다. 이러한 종류의 배열은 알맞은 크기를 가진 아이템 컬렉션을 관리하고 필요할 때 개별 아이템을 얻는 것을 쉽게 해준다.

데이터 구조를 설계할 때 항상 간단한 배열의 집합에서 시작하고 필요하다면 더 능률적인 구조로 옮길 수 있다. 일반적으로 데이터 객체는 절대 퍼포먼스 병목이 되어서는 안된다. 컬렉션 뷰는 보통 모두 합하여 얼마나 많은 객체들이 있는지 계산하고 현재 화면에 보여지는 요소에 대한 뷰를 얻기 위해서만 데이터 소스에 접근한다. 레이아웃 객체가 데이터 객체에서 데이터에만 의존한다면, 데이터 소스가 수천 개의 객체를 포함하고 있을 때 성능은 심각한 영향을 받을 것이다.

컬렉션 뷰에게 컨텐츠에 대해 알리기

컬렉션 뷰가 데이터 소스에게 묻는 질문들은 그것이 가지고 있는 섹션의 개수와 각 섹션이 가지고 있는 아이템의 개수를 포함한다. 컬렉션 뷰는 다음의 액션이 발생할 때 이 정보를 제공하라고 데이터 소스에게 요청한다.

  • 컬렉션 뷰가 처음으로 표시됨
  • 컬렉션 뷰에 다른 데이터 소스 객체를 할당
  • 컬렉션 뷰의 reloadData() 메소드를 직접 호출
  • 컬렉션뷰 델리게이트가 performBatchUpdates(_:completion:)을 사용한 블록 또는 이동, 삽입, 또는 삭제 메소드를 실행

numberOfSections(in:) 메소드를 사용하여 섹션의 개수를, collectionView(_:numberOfItemsInSection:) 메소드를 사용하여 각 섹션에 있는 아이템의 개수를 제공한다. 반드시 collectionView(_:numberOfItemsInSection:) 메소드를 구현해야 하지만 컬렉션 뷰가 오직 하나의 섹션만 가지고 있다면 numberOfSections(in:)의 구현은 선택적이다. 두 메소드 모두 적절한 정보를 가진 정수 값을 반환한다.

위에서 보이는 것처럼 (이차원 배열) 데이터 소스를 구현한다면, 데이터 소스 메소드의 구현은 아래처럼 간단해질 수 있을 것이다. 코드에서 data 변수는 데이터 소스의 커스텀 멤버 변수이며 섹션들의 최상위 배열을 저장한다. 배열의 개수를 구하는 것은 섹션의 개수를 말한다. 하위 배열들 중 하나의 개수를 구하는 것은 그 섹션의 아이템의 개수를 말한다. (물론 코드는 반환 값이 유효한지 보증하기 위해 에러 체크가 필요하다면 해야 한다.)

func numberOfSections(in: UICollectionView) -> Int {
return data.count
}

func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let sectionArray = data[section]
return sectionArray.count
}

셀 및 서플먼터리 뷰 구성하기

데이터 소스의 또다른 중요한 작업은 컬렉션 뷰가 컨텐츠를 표시하기 위해 사용하는 뷰를 제공하는 것이다. 컬렉션 뷰는 앱의 컨텐츠를 추적하지 않는다. 단순히 제공하는 뷰를 취하고 그것에 현재 레이아웃 정보를 적용할 뿐이다. 그러므로 뷰가 표시하는 모든 것은 당신의 책임이다.

데이터 소스가 얼마나 많은 섹션과 아이템을 관리해야 하는지 보고한 후, 컬렉션 뷰는 레이아웃 객체에게 컬렉션 뷰의 컨텐츠에 대한 레이아웃 애트리뷰트를 제공하라고 요청한다. 어떠한 지점에서 컬렉션 뷰는 레이아웃 객체에게 특정 직사각형 (일반적으로 화면에 보이는 직사각형) 안에 있는 요소의 리스트를 제공하라고 요청한다. 컬렉션 뷰는 데이터 소스에게 이에 부합하는 셀과 서플먼터리 뷰를 요청하기 위해 이 리스트를 사용한다. 그러한 셀과 서플먼터리 뷰를 제공하기 위해 코드는 다음의 것들을 따라야 한다.

  1. 스토리보드 파일에 템플릿 셀과 뷰를 추가하라. (대안으로 셀이나 뷰를 지원하는 각 타입에 대한 클래스 또는 xib 파일을 등록하라.)
  2. 데이터 소스에세 요청받을 때 적절한 셀이나 뷰를 디큐하고 구성하라.

셀과 서플먼터리 뷰가 가능한 한 가장 능률적인 방식으로 사용되는 것을 보장하기 위해 컬렉션 뷰는 그러한 객체를 만드는 책임을 당신에게 부과한다. 각각의 컬렉션 뷰는 현재 사용되지 않는 셀과 서플먼터리 뷰에 대한 내부 큐를 유지한다. 객체를 당신이 직접 만드는 대신, 단순히 컬렉션 뷰에게 원하는 뷰를 제공하라고 요청하라. 그것이 재사용 큐에서 대기하고 있다면 컬렉션 뷰는 그것을 준비하고 빠르게 반환한다. 그것이 대기하고 있지 않다면 컬렉션 뷰는 새로운 것을 만들기 위해 등록된 클래스나 xib 파일을 사용하고 반환해준다. 그러므로 셀이나 뷰를 디큐하는 매번, 항상 사용하기에 준비된 객체를 얻게 된다.

재사용 식별자는 여러 종류의 셀과 여러 종류의 서플먼터리 뷰를 등록할 수 있게 해준다. 재사용 식별자는 등록된 셀과 뷰의 타입을 구별하기 위해 사용하는 문자열이다. 문자열의 컨텐츠는 오직 데이터 소스 객체와 관련된다. 하지만 뷰나 셀을 요청할 때 어떠한 타입의 뷰나 셀을 원하는지 결정하기 위해 제공된 인덱스 패스를 사용할 수 있고, 디큐 메소드에 적절한 재사용 식별자를 넘겨줄 수 있다.

셀 및 서플먼터리 뷰 등록하기

컬렉션 뷰에 코드로 셀이나 뷰를 구성할 수 있고, 스토리보드 파일에서도 구성할 수 있다.

스토리보드에서 셀과 뷰를 구성하라. 스토리보드에 셀과 서플먼터리 뷰를 구성한다면, 컬렉션 뷰에 아이템을 드래그한 후 거기에서 구성을 하면 된다. 이것은 컬렉션 뷰와 부합하는 셀 또는 뷰와의 관계를 만든다.

  • 셀에 대하여, 객체 라이브러리에서 Collection View Cell을 드래그하고 컬렉션 뷰에 드랍한다. 셀에 적절한 커스텀 클래스와 컬렉션 재사용 가능 뷰 식별자를 설정한다.
  • 서플먼터리 뷰에 대하여, 객체 라이브러리에서 Collection Reusable View를 드래그하고 컬렉션 뷰에 드랍한다. 뷰에 적절한 커스텀 클래스와 컬렉션 재사용 가능 뷰 식별자를 설정한다.

코드로 셀을 구성하라. register(_:forCellWithReuseIdentifier:) 메소드를 사용하여 재사용 식별자와 함께 셀을 연관시킨다. 부모 뷰 컨트롤러의 초기화 프로세스의 일부분으로 이러한 메소드를 호출할 수 있을 것이다.

코드로 서플먼터리 뷰를 구성하라. register(_:forSupplementaryViewOfKind:withReuseIdentifier:) 메소드를 사용하여 재사용 식별자와 함께 뷰의 각 종류를 연관시킨다. 부모 뷰 컨트롤러의 초기화 프로세스의 일부분으로 이러한 메소드를 호출할 수 있을 것이다.

오직 재사용 식별자만을 사용하여 셀을 등록했다 할지라도, 서플먼터리 뷰는 kind string이라고 알려진 추가적인 식별자를 지정해야 할 필요가 있다. 각각의 레이아웃 객체는 지원하는 서플먼터리 뷰의 kind를 정의할 책임이 있다. 예를 들어 UICollectionViewFlowLayout 클래스는 두 개의 서플먼터리 뷰의 타입, 섹션 헤더 뷰 및 섹션 푸터 뷰를 지원한다. 이 두 개의 종류의 뷰를 식별하기 위해 UICollectionView.elementKindSectionHeader, UIColletionView.elementKindSectionFooter 문자열 상수를 정의한다. 레이아웃 동안 레이아웃 객체는 그 뷰 타입에 대하여 다른 레이아웃 애트리뷰트들과 함께 종류 문자열을 포함한다. 컬렉션 뷰는 데이터 소스에 그 정보를 넘겨준다. 데이터 소스는 어떠한 뷰 객체를 디큐하고 반환할지 결정하기 위해 종류 문자열과 재사용 식별자를 모두 사용한다.

알아두기 : 커스텀 레이아웃을 구현한다면, 레이아웃이 지원하는 서플먼터리 뷰의 종류를 정의하는 것은 당신의 책임이다. 레이아웃은 서플먼터리 뷰를 얼마든지 지원할 수 있으며, 각각은 그 자신의 종류 문자열을 가진다. Creating Custom Layouts에서 커스텀 레이아웃을 정의하는 더 많은 방법에 대해 확인하라.

등록은 셀이나 뷰를 디큐하려는 시도를 하기 전에 반드시 한 번만 일어나야 하는 이벤트다. 등록한 후에야 그것들을 다시 등록하지 않고 필요한 만큼 많은 셀과 뷰를 디큐할 수 있다. 하나 이상의 아이템을 디큐한 후 등록 정보를 변경하는 것을 추천하지 않는다. 한 번 셀과 뷰를 등록하고 그것으로 끝내는 것이 더 낫다.

셀 및 뷰를 디큐하고 구성하기

데이터 소스 객체는 컬렉션 뷰가 요청할 때 셀과 서플먼터리 뷰를 제공할 책임이 있다. UICollectionViewDataSource 프로토콜은 이 목적을 위해 두 개의 메소드, collectionView(_:cellForItemAt:)collectionView(_:viewForSupplementaryElementOfKind:at:) 메소드를 포함한다. 셀은 컬렉션 뷰의 필수 요소이기 때문에 데이터 소스는 collectionView(_:cellForItemAt:) 메소드를 반드시 구현해야 한다. 하지만 collectionView(_:viewForSupplementaryElementOfKind:at:) 메소드는 선택적이며 사용하는 레이아웃의 종류에 달려 있다. 두 개의 모든 케이스에 대하여 이 메소드들의 구현은 매우 단순한 다음의 패턴을 따른다.

  1. dequeueReusableCell(withReuseIdentifier:for:) 또는 dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:) 메소드를 사용하여 적절한 타입의 셀 또는 뷰를 디큐한다.
  2. 지정된 인덱스 패스에 있는 데이터를 사용하여 뷰를 구성한다.
  3. 뷰를 반환한다.

디큐 프로세스는 당신이 셀이나 뷰를 직접 만들어야 하는 책임을 덜어주기 위해 고안되었다. 이전에 셀이나 뷰를 등록했다면, 디큐 메소드는 절대 nil을 반환하지 않을 것임을 보장한다. 재사용 큐에 주어진 타입에 대한 셀이나 뷰가 존재하지 않는다면, 디큐 메소드는 스토리보드나 등록한 클래스 또는 xib 파일을 사용하여 하나를 만들어낸다.

디큐 프로세스에서 반환된 셀은 초기의 상태에 있어야 하고 새로운 데이터를 사용하여 구성될 준비가 되어 있어야 한다. 만들어져야 하는 셀이나 뷰에 대하여, 디큐 프로세스는 일반적인 프로세스를 사용하여 그것을 만들고 초기화한다. 즉 스토리보드나 xib 파일에서 뷰를 로드하거나, 또는 init(frame:) 메소드를 사용하여 새로운 인스턴스를 만들고 초기화한다. 대조적으로 처음부터 만들어지지 않고 재사용 큐에서 되찾아온 아이템은 이미 이전 사용에서의 데이터를 가지고 있다. 이 경우에 디큐 메소드는 아이템의 prepareForReuse() 메소드를 호출하여 그 자신을 초기 상태로 되돌릴 기회를 제공한다. 커스텀 셀 또는 뷰 클래스를 구현한다면, 이 메소드를 재정의하여 프로퍼티들을 기본값으로 재설정하고 추가적인 정리 작업을 수행할 수 있다.

데이터 소스가 뷰를 디큐한 후, 새로운 데이터로 뷰를 구성한다. 적절한 데이터 객체의 위치를 찾고 뷰에 객체의 데이터를 적용하기 위해 데이터 소스 메소드가 전달한 인덱스 패스를 사용할 수 있다. 뷰를 구성한 후 메소드에서 그것을 반환하면 모든 작업이 완료된다. 다음은 셀을 구성하는 방법에 대한 간단한 예제다. 셀을 디큐한 후 메소드는 셀의 위치와 관련된 정보를 사용하여 셀의 커스텀 레이블을 설정하고 그 셀을 반환한다.

func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let newCell = collectionView.dequeueReusableCell(withReuseIdentifier: myCellID, for: indexPath) as! MyCustomCell
newCell.cellLabel.text = "Section:\(indexPath.section), Item:\(indexPath.item)"
return newCell
}

알아두기 : 데이터 소스에서 뷰를 반환할 때 항상 유효한 뷰를 반환하라. 요청받은 뷰가 표시될 수 없는 등의 어떠한 이유에서든 nil을 반환한다면, 레이아웃 객체는 이 메소드들이 유효한 뷰를 반환하기를 기대하기 때문에 assertion이 발생하여 앱은 종료된다.

섹션 및 아이템을 삽입, 삭제, 이동하기

하나의 섹션 또는 아이템을 삽입, 삭제, 이동하기 위해 다음의 단계를 따라라.

  1. 데이터 소스 객체에서 데이터를 갱신한다.
  2. 섹션이나 아이템을 삽입하거나 삭제하기 위해 컬렉션 뷰의 적절한 메소드를 호출한다.

컬렉션 뷰에게 어떠한 변화가 있는지 알리기 전에 데이터 소스를 갱신하는 것은 중요하다. 컬렉션 뷰 메소드는 데이터 소스가 현재 올바른 데이터를 가지고 있다고 가정한다. 그렇지 않다면 컬렉션 뷰는 데이터 소스에서 잘못된 아이템 집합을 받거나 거기에 있지 않은 아이템을 요청할 것이며 앱을 충돌시킨다.

코드로 하나의 아이템을 추가, 삭제, 또는 이동할 때 컬렉션 뷰의 메소드는 자동으로 그 변화를 반영하기 위해 애니메이션을 만든다. 그럼에도 불구하고, 여러 개의 변화에 한 번에 애니메이션 효과를 주고 싶다면, 반드시 performBatchUpdates(_:completion:) 메소드를 사용하여 모든 삽입, 삭제, 이동 호출을 블록 내에서 수행하고, 그 블록을 메소드에 넘겨주어야 한다. 그러면 일괄 갱신 프로세스는 같은 시간에 모든 변화에 대한 애니메이션을 처리하며, 자유롭게 같은 블록 내에서 아이템의 삽입, 삭제, 이동 호출을 혼합할 수 있다.

다음은 현재 선택된 아이템을 삭제하기 위해 일괄 갱신을 수행하는 방법에 대한 간단한 예제다. performBatchUpdates(_:completion:) 메소드에 넘겨진 블록은 먼저 데이터 소스를 갱신하기 위해 커스텀 메소드를 호출한다. 그리고 나서 컬렉션 뷰에게 아이템을 삭제하라고 알린다. 갱신 블록과 컴플리션 블록은 모두 동기적으로 실행된다.

collectionView.performBatchUpdates({
let itemPaths = self.collectionView.indexPathsForSelectedItems
self.deleteItemsFromDataSource(at: itemPaths)
self.collectionView.deleteItems(at: itemPaths)
}, completion: nil)

선택 및 하이라이팅에 대한 시각적 상태 관리하기

컬렉션 뷰는 기본적으로 단일 아이템 선택을 지원하며, 여러 개의 아이템의 선택을 지원하기 위해 구성될 수 있으며, 선택을 완전히 비활성화할 수 있다. 컬렉션 뷰는 그 바운드 내의 탭을 감지하며, 이에 따라 부합하는 셀을 하이라이팅하거나 선택한다. 대부분의 경우 컬렉션 뷰는 오직 그것이 선택되었거나 하이라이팅되었는지를 가리키기 위해 셀의 프로퍼티들만을 수정할 뿐이다. 한 가지 예외를 제외하고, 셀의 시각적 모습을 변경하지 않는다. 셀의 selectedBackgroundView 프로퍼티가 유효한 뷰를 포함하고 있다면, 컬렉션 뷰는 셀이 하이라이팅되었거나 선택되었을 때 그 뷰를 보여준다.

다음은 하이라이팅 또는 선택 상태에 대하여 모습을 바꾸는 것을 촉진시키기 위해 커스텀 컬렉션 뷰 셀의 구현에 통합한 것을 보여준다. 셀의 backgroundView 프로퍼티는 항상 셀이 처음에 로드되고, 셀이 하이라이팅되지 않거나 선택되지 않을 때 기본 뷰가 된다. selectedBackgroundView 프로퍼티는 셀이 하이라이팅되거나 선택될 때마다 기본 배경 뷰를 교체한다. 이 경우 셀의 배경색은 선택되거나 하이라이팅될 때 빨간색에서 하얀색으로 변화할 것이다.

let backgroundView = UIView(frame: bounds)
backgroundView.backgroundColor = .red
self.backgroundView = backgroundView

let selectedBGView = UIView(frame: bounds)
selectedBGView.backgroundColor = .white
self.selectedBackgroundView = selectedBGView

컬렉션 뷰의 델리게이트는 하이라이팅과 선택을 촉진시키기 위해 다음의 메소드를 제공한다.

  • collectionView(_:shouldSelectItemAt:)
  • collectionView(_:shouldDeselectItemAt:)
  • collectionView(_:didSelectItemAt:)
  • collectionView(_:didDeselectItemAt:)
  • collectionView(_:shouldHighlightItemAt:)
  • collectionView(_:didHighlightItemAt:)
  • collectionView(_:didUnhighlighgItemAt:)

이 메소드들은 컬렉션 뷰가 정확히 바라는 명세에 맞는 하이라이팅 및 선택 동작을 맞추기 위한 많은 기회를 제공한다.

예를 들어 셀의 선택 상태를 직접 그리기 원한다면, selectedBackgroundView 프로퍼티를 nil로 설정하고 델리게이트 객체를 사용하여 셀에 어떠한 시각적 변화를 적용할 수 있다. collectionView(_:didSelectItemAt:) 메소드에서 시각적 변화를 적용하고, collectionView(_:didDeselectItemAt:) 메소드에서 그것을 제거할 수 있을 것이다.

하이라이팅 상태를 직접 그리는 것을 선호한다면, collectionView(_:didHighlightItemAt:)collectionView(_:didUnhighlightItemAt:) 델리게이트 메소드를 재정의하여 하이라이팅 효과를 적용하는 데 사용할 수 있다. selectedBackgroundView 프로퍼티도 지정한다면, 셀의 컨텐츠 뷰에 대한 변화는 그 변화가 가시적인 것을 보장하도록 해야 한다. 다음은 컨텐츠 뷰의 배경 색상을 사용하여 하이라이팅을 변경하는 간단한 방법을 보여준다.

func collectionView(_ collectionView: UICollectioView, didHighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell.contentView.backgroundColor = .blue
}

func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell.contentView.backgroundColor = nil
}

셀의 하이라이팅 상태와 선택 상태를 구분하는 것은 미묘하나 중요하다. 하이라이팅 상태는 사용자의 손가락이 여전히 디바이스를 터치하고 있는 동안 셀에 가시적인 하이라이팅 효과를 적용하기 위해 사용할 수 있는 변천 상태다. 이 상태는 컬렉션 뷰가 셀에 대한 터치 이벤트를 추적하고 있는 동안에만 true로 설정된다. 터치 이벤트가 멈출 때 하이라이팅 상태는 false 값을 반환한다. 대조적으로 선택 상태는 오직 일련의 터치 이벤트가 종료한 이후에 변화한다. 특히 그러한 터치 이벤트들은 사용자가 셀을 선택하려고 할 때를 가리킨다.

최초의 터치다운 이벤트는 컬렉션 뷰가 셀의 하이라이팅 상태를 true로 변경하는 원인이 된다. 그럼에도 불구하고 이는 셀의 모습을 자동으로 변경하지 않는다. 최종의 터치업 이벤트가 셀에서 발생하면, 하이라이팅 상태는 false를 반환하고 컬렉션 뷰는 선택 상태를 true로 변경한다. 사용자가 선택 상태를 변경할 때 컬렉션 뷰는 셀의 selectedBackgroundView 프로퍼티에 있는 뷰를 표시하지만, 이는 컬렉션 뷰가 셀에 대하여 만드는 시각적인 변화일 뿐이다. 다른 시각적 변화들은 반드시 델리게이트 객체가 만들어야 한다.

사용자가 셀을 선택하든 선택을 취소하든, 셀의 선택 상태는 항상 변화에 대한 마지막 상태가 된다. 셀에 대한 탭은 항상 먼저 셀의 하이라이팅 상태를 변경한다. 오직 탭 시퀀스가 끝나고 그 시퀀스 동안 적용된 하이라이팅이 제거된 후에야, 셀의 선택 상태가 변화한다. 셀을 디자인할 때, 하이라이팅 및 선택 상태에 대한 시각적 모습이 의도하지 않은 방식으로 충돌하지 않게 해야 한다.

셀에 대하여 편집 메뉴 표시하기

사용자가 셀에 롱 탭 제스처를 수행할 때 컬렉션 뷰는 그 셀에 대해 편집Edit 메뉴를 표시하려고 한다. 편집 메뉴는 컬렉션 뷰에 있는 셀을 자르고, 복사하고, 붙여넣기 위해 사용될 수 있다. 편집 메뉴가 표시될 수 있기 위해 충족되어야 하는 몇 가지 조건이 있다.

  • 델리게이트는 액션 핸들링과 관련된 세 개의 메소드를 모두 구현해야 한다.

    • collectionView(_:shouldShowMenuForItemAt:)
    • collectionView(_:canPerformAction:forItemAt:withSender:)
    • collectionView(_:performAction:forItemAt:withSender:)
  • collectionView(_:shouldShowMenuForItemAt:) 메소드는 바람직한 셀에 대하여 반드시 true를 반환해야 한다.

  • collectionView(_:canPerformAction:forItemAt:withSender:) 메소드는 적어도 바람직한 액션들 중 하나에 대하여 true를 반환해야 한다. 컬렉션 뷰는 다음의 액션을 지원한다.

    • cut:
    • copy:
    • paste:

이러한 조건이 충족되고 사용자가 메뉴에서 액션을 선택하면, 컬렉션 뷰는 델리게이트의 collectionView(_:performAction:forItemAt:withSender:) 메소드를 호출하여 바람직한 아이템에 대하여 해당 액션을 수행한다.

다음은 메뉴 아이템들 중 하나가 나타나지 못하게 하는 방법을 보여준다. 예제에서 collectionView(_:canPerformAction:forItemAt:withSender:) 메소드는 오려두기Cut 메뉴 아이템이 편집 메뉴에 나타나지 못하게 한다. 사용자가 컨텐츠를 삽입할 수 있게 하기 위해 복사하기Copy 아이템과 붙여넣기Paste 아이템을 활성화한다.

func collectionView(_: UIcollectionView, canPerformAction: Selector, forItemAt: IndexPath, withSender: Any?) -> Bool {
if NSStringFromSelector(action) == "copy:" || NSStringFromSelector(action) == "paste:" {
return true
}
return false
}

Text Programming Guide for iOS에서 페이스트보드 명령으로 작업하는 것과 관련된 더 많은 정보를 확인하라.

이 섹션에서 소개된 메소드들은 iOS 13에서 deprecated되었다. 대신 Context Menu를 사용하는 메소드들을 사용할 수 있다.

레이아웃 간 전환하기

레이아웃 간 전환을 하는 가장 쉬운 방법은 setCollectionViewLayout(_:animated:) 메소드를 사용하는 것이다. 전환에 대한 제어가 필요하거나 이를 상호 작용 가능하게 하고 싶다면 UICollectionViewTransitionLayout 객체를 사용하라.

UICollectionViewTransitionLayout 클래스는 새로운 레이아웃으로 전환할 때 컬렉션 뷰의 레이아웃 객체에 적용되는 특별한 타입의 레이아웃이다. 레이아웃 객체의 전환 시에 비선형 경로를 따르거나, 다른 타이밍 알고리즘을 사용하거나, 다가오는 터치 이벤트에 따라 움직이게 할 수 있다. 표준 클래스는 새로운 레이아웃에 선형 전환을 제공하지만, UICollectionViewLayout 클래스와 마찬가지로 UICollectionViewTransitionLayout 클래스는 서브클래싱하여 바람직한 효과를 만들어낼 수 있다. 그렇게 하기 위해 커스텀 레이아웃을 만들기 위해 구현해야 했던 것과 같은 메소드들을 구현할 필요가 있고, 구현이 사용자로부터의 입력에 적응할 수 있게 해야 한다. 사용자로부터의 입력은 보통 제스처 레코그나이저로부터 일어난다. Creating Custom Layouts에서 커스텀 레이아웃 객체를 만드는 것에 대한 더 많은 정보를 확인하라.

UICollectionViewLayout 클래스는 레이아웃 간 전환을 추적하기 위한 몇 가지 메소드를 제공한다. UICollectionViewTransitionLayout 객체는 transitionProgress 프로퍼티를 통해 전환의 완료를 추적한다. 전환이 일어날 때 코드는 주기적으로 이 프로퍼티를 갱신하여 전환의 완료 비율을 가리켜야 한다. 예를 들어 레이아웃 간 전환에 사용하기 위한 제스처 레코그나이저와 같은 객체와 UICollectionViewTransitionLayout 클래스를 결합하여 사용하는 것은 상호 작용 가능한 전환을 만들 수 있게 해준다. 또한 커스텀 전환 레이아웃 객체를 구현한다면 UICollectionViewTransitionLayout 클래스는 레이아웃과 관련된 값들을 추적하기 위한 두 개의 메소드, updateValue(_:forAnimatedKey:), value(forAnimatedKey:) 메소드를 제공한다. 이 메소드들은 레이아웃과 중요한 정보를 전달하기 위해 설정할 수 있고 전환 동안 변화할 수 있는 특별한 부동 소수점 값을 추적한다. 예를 들어 핀치 제스처를 사용하여 레이아웃 간 전환될 때, 이 메소드들을 사용하여 전환 레이아웃 객체에 뷰가 또다른 것으로부터 필요로 하는 오프셋이 무엇인지 알려줄 수 있을 것이다.

UICollectionViewTransitionLayout 객체를 포함한다면 다음의 단계를 따라야 한다.

  1. init(currentLayout:nextLayout) 메소드를 사용하여 표준 클래스 또는 커스텀 클래스의 인스턴스를 생성한다.
  2. transitionProgress 프로퍼티를 주기적으로 수정하여 전환의 프로그레스를 전달한다. 전환의 프로그레스를 변경한 후 컬렉션 뷰의 invalidateLayout() 메소드를 사용하여 레이아웃을 무효로 만드는 것을 잊지 마라.
  3. collectionView(_:transitionLayoutForOldLayout:newLayout:) 메소드를 컬렉션 뷰의 델리게이트에 구현하고 전환 레이아웃 객체를 반환한다.
  4. 선택적으로 updateValue(_:forAnimatedKey:) 메소드를 사용하여, 레이아웃 객체와 관련된 변경된 값을 알리기 위해 레이아웃에 대한 값을 변경한다. 이 경우 안정적인 값은 0이다.

플로우 레이아웃 사용하기

구체적인 레이아웃 객체, UICollectionViewFlowLayout을 사용하여 컬렉션 뷰에서 아이템을 정렬할 수 있다. 플로우 레이아웃은 라인 기반 분리 레이아웃을 구현하며, 이는 레이아웃 객체가 셀들을 선형 경로에 배치하고 그 라인에 최대한 많은 셀이 들어갈 수 있도록 맞춘다는 것을 의미한다. 레이아웃 객체가 현재 라인에서 사용 가능한 공간을 모두 사용했을 때 새로운 라인을 만들고 거기에서 레이아웃 프로세스를 지속한다. 플로우 레이아웃이 수직 방향으로 스크롤되는 경우에, 라인은 수직 방향으로 놓여지며 각각의 새로운 라인은 이전 라인의 아래에 위치한다. 하나의 섹션에 있는 셀들은 선택적으로 섹션 헤더 뷰 및 섹션 푸터 뷰에 둘러싸여 있을 수 있다.

격자 모양을 구현하기 위해 플로우 레이아웃을 사용할 수 있으나, 더 많은 것들을 할 수도 있다. 선형 레이아웃의 아이디어는 많은 다른 디자인에 적용할 수 있다. 예를 들어 아이템의 격자를 갖기보다는 스크롤되는 차원을 따라 아이템의 하나의 라인을 만들기 위해 간격을 조정할 수 있다. 아이템들은 또한 다른 크기를 가질 수 있으며, 이는 전형적인 격자 모양보다 더욱 비대칭적이지만 여전히 선형 플로우를 가지고 있다. 여기에는 많은 기회가 있다.

코드로, 또는 Xcode의 인터페이스 빌더를 사용하여 플로우 레이아웃을 구성할 수 있다. 플로우 레이아웃을 구성하는 단계는 다음과 같다.

  1. 플로우 레이아웃 객체를 만들고 컬렉션 뷰에 할당한다.
  2. 셀의 너비 및 높이를 구성한다.
  3. 필요하다면 라인과 아이템에 대한 간격 옵션을 설정한다.
  4. 섹션 헤더나 섹션 푸터를 원한다면 그것들의 크기를 지정한다.
  5. 레이아웃에 대한 스크롤 방향을 설정한다.

중요 : 최소한 셀의 너비와 높이를 반드시 지정해야 한다. 그렇지 않다면 아이템의 너비와 높이는 0으로 설정되어 절대 보이지 않게 될 것이다.

플로우 레이아웃 애트리뷰트 커스터마이징하기

플로우 레이아웃 객체는 컨텐츠의 외관을 구성하기 위한 몇 가지 프로퍼티들을 드러낸다. 설정될 때 이 프로퍼티들은 레이아웃에 있는 모든 아이템들에 같게 적용된다. 예를 들어 플로우 레이아웃 객체의 itemSize 프로퍼티를 사용하여 셀의 크기를 설정하면 모든 셀은 같은 크기를 같게 된다.

아이템의 간격이나 크기를 동적으로 다양하게 하고 싶다면 UICollectionViewDelegateFlowLayout 프로토콜에 있는 메소드들을 사용하여 할 수 있다. 컬렉션 뷰 그 자체에 할당된 같은 델리게이트 객체에서 이러한 메소드들을 구현한다. 주어진 메소드가 존재한다면 플로우 레이아웃 객체는 그것이 가진 고정 값을 사용하는 대신 이 메소드를 호출한다. 구현은 컬렉션 뷰의 모든 아이템에 대하여 반드시 적절한 값을 반환해야 한다.

플로우 레이아웃 내의 아이템들의 크기 지정하기

컬렉션 뷰에 있는 모든 아이템이 같은 크기를 갖는다면, 플로우 레이아웃 객체의 itemSize 프로퍼티에 적절한 너비와 높이 값을 할당하라. (항상 포인트 단위에서 아이템의 크기를 지정하라.) 이는 크기가 다양하지 않은 컨텐츠에 대하여 레이아웃 객체를 구성하는 가장 빠른 방법이다.

셀에 대하여 다른 크기를 지정하고 싶다면, collectionView(_:layout:sizeForItemAt:) 메소드를 컬렉션 뷰 델리게이트에서 반드시 구현해야 한다. 부합하는 아이템에 대한 크기를 반환하기 위해 제공된 인덱스 패스 정보를 사용할 수 있다. 레이아웃 동안 플로우 레이아웃 객체는 스크롤 방향이 수직 방향일 때, 같은 라인에서 아이템을 수직 방향으로 중앙 정렬한다. 라인의 전체 너비나 높이는 그 차원에서 가장 큰 아이템이 결정한다.

알아두기 : 셀에 대하여 다른 크기를 지정한다면, 하나의 라인에 대한 아이템의 개수는 라인마다 다를 수 있다.

아이템 및 라인 간 간격 지정하기

플로우 레이아웃을 사용할 때 같은 라인에 있는 아이템들 간의 최소 간격과, 연속적인 라인 간의 최소 간격을 지정해야 한다. 제공하는 간격은 오직 최소 간격이라는 것을 염두에 두어라. 컨텐츠를 배치하는 방법 때문에 플로우 레이아웃 객체는 지정한 값보다 더 큰 값을 아이템 간의 간격으로 설정할 수 있다. 레이아웃 객체는 아이템이 다른 크기로 배치될 때 실제 라인 간격도 비슷하게 증가시켜서 설정할 수 있다.

레이아웃 동안 플로우 레이아웃 객체는 전체 아이템을 채우기 위한 충분한 공간이 더 이상 남아 있지 않을 때까지 현재 라인에 아이템들을 추가한다. 라인이 여분의 공간 없이 정수 개수의 아이템들을 충분히 채울 만큼 커진다면, 아이템 간의 공간은 최소 간격과 같아지게 될 것이다. 라인의 끝에 여분의 공간이 있다면, 레이아웃 객체는 라인 경계 내에서 아이템들이 동일하게 적합해질 때까지 아이템 간 간격을 늘린다. 간격을 늘리는 것은 아이템의 전체 겉모습을 향상시키고 각 라인의 끝에 있는 커다란 공백을 없앤다.

라인 간 간격에 대해서, 플로우 레이아웃은 아이템 간 간격에서 적용한 것과 같은 테크닉을 사용한다. 모든 아이템이 동일한 크기를 가진다면, 플로우 레이아웃은 완전하게 최소 라인 간 간격 값을 반영할 수 있고, 하나의 라인에 있는 모든 아이템들은 다음 라인에 있는 아이템들과 동일한 간격을 갖게 된다. 아이템들이 다른 크기를 가진다면, 개별 아이템들 간 실제 간격은 다양해질 수 있다.

다른 크기를 갖는 아이템에 대하여, 플로우 레이아웃 객체는 각 라인에서 스크롤 방향에 대한 치수에서 가장 큰 아이템을 고른다. 예를 들어 수직 방향으로 스크롤되는 레이아웃에서는 각 라인에서 가장 큰 높이를 갖는 아이템을 찾는다. 그리고 나서 그 아이템들 사이의 간격을 최소 값으로 설정한다. 그 아이템들이 라인의 다른 부분에 있다면, 실제 라인 간격은 최소값보다 더 커진 것처럼 보일 수 있다.

다른 플로우 레이아웃 애트리뷰트처럼, 간격 값을 고정시키거나 동적으로 변화하게 할 수 있다. 라인 간격 및 아이템 간격은 섹션 간에서 처리된다. 그러므로 라인 간격와 아이템 간 간격은 주어진 섹션에서 모든 아이템에 대해 동일하지만 섹션 간에서는 달라질 수 있다. 플로우 레이아웃 객체의 minimumLineSpacingminimumInteritemSpacing 프로퍼티를 사용하거나, 컬렉션 뷰 델리게이트의 collectionView(_:layout:minimumLineSpacingForSectionAt:)collectionView(_:layout:minimumInteritemSpacingForSectionAt:) 메소드를 사용하여 간격을 정적으로 설정한다.

컨텐츠의 마진을 비틀기 위해 섹션 인셋 사용하기

섹션 인셋은 셀을 배치하기 위해 사용 가능한 공간을 조절하기 위한 방법이다. 섹션의 헤더 뷰 이후 또는 푸터 뷰 이전에 공간을 삽입하기 위해 인셋을 사용할 수 있다. 또한 컨텐츠의 양옆에 공간을 삽입하기 위해 인셋을 사용할 수도 있다.

인셋은 셀을 배치하기 위해 사용 가능한 공간의 양을 줄이기 때문에, 주어진 라인에서 셀의 개수를 제한하기 위해 사용할 수 있다. 스크롤되지 않는 방향에서 인셋을 지정하여 각 라인에 대한 공간을 수축시킬 수 있다. 이 정보를 적절한 셀 크기 정보와 결합한다면, 각 라인에 있는 셀의 개수를 제어할 수 있다.

플로우 레이아웃을 서브클래싱해야 할 때를 알기

서브클래싱 없이 플로우 레이아웃을 매우 효과적으로 사용할 수 있을지라도, 필요로 하는 동작을 얻기 위해 서브클래싱해야 할 필요가 있을 수 있다. 다음은 UICollectionViewFlowLayout을 서브클래싱하는 것이 바람직한 효과를 얻기 위해 필수적인 시나리오를 나타낸다.

시나리오 서브클래싱 팁
레이아웃에 새로운 서플먼터리 뷰나 데코레이션 뷰를 추가하고 싶다. 표준 플로우 레이아웃 클래스는 오직 섹션 헤더 뷰와 섹션 푸터 뷰를 지원하고, 데코레이션 뷰는 지원하지 않는다. 추가적으로 서플먼터리 뷰와 데코레이션 뷰를 지원하기 위해 최소한 다음의 메소드를 재정의해야 한다.
- layoutAttributesForElements(in:) (필수)
- layoutAttributesForItem(at:) (필수)
- layoutAttributesForSupplementaryView(ofKind:at:) (새로운 서플먼터리 뷰를 지원하기 위함)
- layoutAttributesForDecorationView(ofKind:at:) (새로운 데코레이션 뷰를 지원하기 위함)
layoutAttributesForElements(in:) 메소드에서 셀에 대한 레이아웃 애트리뷰트를 얻기 위해 super를 호출할 수 있으며, 그리고 나서 지정된 직사각형 내에 있는 새로운 서플먼터리 뷰나 데코레이션 뷰에 대한 애트리뷰트를 추가할 수 있다. 필요 시에 애트리뷰트를 제공하기 위해 다른 메소드를 사용하라.
Creating Layout Attributes 및 Providing Layout Attributes for Items in a Given Rectangle에서 레이아웃 동안 뷰에 대한 애트리뷰트를 제공하는 것과 관련된 정보를 확인하라.
플로우 레이아웃이 반환하는 레이아웃 애트리뷰트를 비틀고 싶다. layoutAttributesForElements(in:) 메소드와 레이아웃 애트리뷰트를 반환하는 메소드들을 재정의한다. 메소드들의 구현은 super를 호출해야 하며, 부모 클래스가 제공한 애트리뷰트를 수정하고 그것들을 반환한다.
Creating Layout Attributes와 Providing Layout Attributes for Items in a Given Rectangle에서 이 메소드들이 필요로 하는 것에 대한 깊은 논의가 이루어진다.
셀과 뷰에 대하여 새로운 레이아웃 애트리뷰트를 추가하고 싶다. UICollectionViewLayoutAttributes의 커스텀 서브클래스를 만들고 커스텀 레이아웃 정보를 표현하기 위해 필요한 프로퍼티를 추가한다.
UICollectionViewFlowLayout을 서브클래싱하고 layoutAttributesClass 프로퍼티를 재정의한다. 이 프로퍼티의 구현에서 커스텀 서브클래스를 반환한다.
또한 layoutAttributeForElements(in:)layoutAttributesForItem(at:) 메소드, 그리고 레이아웃 애트리뷰트를 반환하는 다른 메소드들을 재정의해야 한다. 커스텀 구현에서 당신이 정의한 커스텀 애트리뷰트들에 값을 설정해야 한다.
추가되거나 삭제되는 아이템에 대하여 초기 위치나 최종 위치를 지정하고 싶다. 기본적으로 아이템이 삽입되거나 삭제될 때 간단한 페이드 애니메이션이 만들어진다. 커스텀 애니메이션을 만들고 싶다면, 다음의 메소드들 중 일부나 모두를 반드시 재정의해야 한다.
- initialLayoutAttributesForAppearingItem(at:)
- initialLayoutAttributesForAppearingSupplementaryElement(ofKind:at:)
- initialLayoutAttributesForAppearingDecorationElements(ofKind:at:)
- finalLayoutAttributesForDisappearingItem(at:)
- finalLayoutAttributesForDisappearingSupplementaryElement(ofKind:at:)
- finalLayoutAttributesForDisappearingDecorationElement(ofKind:at:)
이 메소드들의 구현에서 각 뷰가 삽입되기 전이나 제거된 후에 갖기를 원하는 애트리뷰트를 지정한다. 플로우 레이아웃 객체는 삽입과 삭제에 애니메이션 효과를 주기 위해 제공한 애트리뷰트를 사용한다.
이 메소드들을 재정의한다면, prepare(forCollectionViewUpdates:)finalizeCollectionViewUpdates() 메소드 또한 재정의하는 것을 추천한다. 현재 사이클 동안 어떤 아이템이 삽입되거나 삭제될 것인지 추적하기 위해 이 메소드들을 사용할 수 있다.
Making Insertion and Deletion Animations More Interesting에서 삽입 작업과 삭제 작업에 대한 더 많은 정보를 확인하라.

처음부터 커스텀 레이아웃을 만드는 것도 올바른 것을 하기 위한 예시가 될 수 있다. 이것을 하기로 결정하기 전에 이것이 정말로 필요로 한 것인지 고려할 충분한 시간을 가져라. 플로우 레이아웃은 많은 다른 종류의 레이아웃에 적절한 커스터마이징 가능한 많은 동작을 제공하며, 사용하기 쉽고 능률적으로 하기 위한 많은 최적화를 포함하고 있다. 그러나 이것은 절대 커스텀 레이아웃을 만들지 마라는 말이 아니다. 그렇게 하는 것이 절대적으로 더 좋은 상황이 있기 때문이다. 플로우 레이아웃은 스크롤 방향을 한 방향으로 제한하므로, 레이아웃이 스크린의 양방향으로 뻗어가는 컨텐츠를 포함한다면 커스텀 레이아웃을 구현하는 것이 더 맞는 것이 된다. 레이아웃이 격자 모양이나 라인 기반 분리 레이아웃이 아니라면, 또는 레이아웃 내의 아이템들이 매우 주기적으로 이동하여 플로우 레이아웃을 서브클래싱하는 것이 당신만의 레이아웃을 만드는 것보다 더 복잡하다면, 커스텀 레이아웃을 만드는 것은 올바른 결정이 된다.

Creating Custom Layouts에서 커스텀 레이아웃을 만드는 것에 관한 더 많은 정보를 확인하라.

제스처 지원하기

제스처 레코그나이저를 사용하여 컬렉션 뷰에 더 나은 상호 작용성을 추가할 수 있다. 컬렉션 뷰에 제스처 레코그나이저를 추가하고, 제스처가 발생할 때 트리거할 액션을 사용한다. 컬렉션 뷰에 대하여 구현하기를 원할 것 같은 두 가지 종류의 액션이 있다.

  • 컬렉션 뷰의 레이아웃 정보 변경을 트리거하고 싶다.
  • 셀과 뷰를 직접 조절하고 싶다.

항상 특정 셀이나 뷰가 아닌, 컬렉션 뷰 자체에 제스처 레코그나이저를 부착해야 한다. UICollectionView 클래스는 UIScrollView의 자식 클래스이므로, 제스처 레코그나이저를 컬렉션 뷰에 부착하는 것은 추적되어야 하는 다른 제스처들과의 간섭 가능성을 줄일 수 있다. 게다가 컬렉션 뷰는 데이터 소스와 레이아웃 객체에 대해 접근할 수 있기 때문에, 여전히 셀과 뷰를 적절하게 조절하기 위해 필요로 하는 모든 정보에 접근할 수 있다.

레이아웃 정보를 변경하기 위해 제스처 레코그나이저 사용하기

제스처 레코그나이저는 동적으로 레이아웃 매개변수를 수정할 수 있는 쉬운 방법을 제공한다. 예를 들어 커스텀 레이아웃에서 아이템 간 간격을 변경하기 위해 핀치 제스처 레코그나이저를 사용할 수 있을 것이다. 제스처 레코그나이저와 같은 구성을 위한 프로세스는 상대적으로 간단한다.

  1. 제스처 레코그나이저를 만든다.
  2. 컬렉션 뷰에 제스처 레코그나이저를 부착한다.
  3. 레이아웃 매개변수를 갱신하고 레이아웃 객체를 무효화하기 위해 제스처 레코그나이저의 핸들러 메소드를 사용한다.

모든 객체에서 사용한 것처럼 동일한 init 프로세스를 사용하여 제스처 레코그나이저를 만든다. 초기화 동안 타겟 객체와 제스처가 트리거될 때 호출할 액션 메소드를 지정한다. 그리고 나서 컬렉션 뷰의 addGestureRecognizer(_:) 메소드를 호출하여 뷰에 제스처 레코그나이저를 부착한다. 실제 작업 중 대부분은 초기화 시점에 지정한 액션 메소드에서 일어난다.

다음은 컬렉션 뷰에 부착된 핀치 제스처 레코그나이저가 호출한 액션 메소드의 예시를 보여준다. 이 예제에서 핀치 데이터는 커스텀 레이아웃에서 셀 간의 간격을 변경하기 위해 사용된다. 레이아웃 객체는 커스텀 updateSpreadDistance(_:) 메소드를 구현하였고, 이는 새로운 거리 값을 검증하고 나중에 레이아웃 프로세스에서 사용하기 위해 값을 저장한다. 그리고 나서 액션 메소드는 레이아웃을 무효화하고 새로운 값에 기반하여 아이템의 위치를 갱신하는 것을 강제한다.

func handlePinchGesture(_ sender: UIPinchGestureRecognizer) {
if sender.numberOfTouches != 2 { return }
let p1 = sender.location(ofTouch: 0, in: collectionView)
let p2 = sender.location(ofTouch: 1, in: collectionView)
let xd = p1.x - p2.x
let yd = p1.y - p2.y
let distance = sqrt(xd * xd + yd * yd)
let myLayout = collectionView.collectionViewLayout as! MyCustomLayout
myLayout.updateSpreadDistance(distance)
myLayout.invalidateLayout()
}

Event Handling Guide for iOS에서 제스처 레코그나이저를 만들고 뷰에 부착하는 것과 관련된 더 많은 정보를 확인하라.

기본 제스처 동작으로 작업하기

UICollectionView 클래스는 하이라이팅과 선택에 대한 델리게이트 메소드를 개시하기 위해 단일 탭을 감지한다. 컬렉션 뷰에 커스텀 탭 제스처나 롱 프레스 제스처를 추가하고 싶다면 컬렉션 뷰가 이미 사용하고 있는 값과는 다른 값을 제스처 레코그나이저에 구성하라. 예를 들어 탭 제스처 레코그나이저가 오직 더블 탭에 대해서만 반응하도록 구성할 수 있을 것이다.

다음은 컬렉션 뷰가 셀 선택 및 하이라이팅에 감지하는 대신, 당신의 제스처에 반응하도록 하는 방법을 보여준다. 컬렉션 뷰는 델리게이트 메소드를 개시하기 위해 제스처 레코그나이저를 사용하지 않으므로, 커스텀 제스처 레코그나이저는 제스처 레코그나이저의 delaysTouchesBegan 프로퍼티를 true로 설정하여 다른 터치 이벤트의 등록을 지연시키거나, cancelsTouchesInView 프로퍼티를 true로 설정하여 터치 이벤트를 취소하는 방식으로 기본 선택 리스너보다 높은 우선순위를 갖게 된다. 탭이 등록될 때마다 먼저 제스처 레코그나이저가 우선순위를 갖지고 있는지를 먼저 확인할 것이다. 입력이 제스처 레코그나이저에 대하여 유효하지 않다면, 델리게이트 메소드는 그래왔던 것처럼 호출될 것이다.

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
tapGesture.delaysTouchesBegan = true
tapGesture.numberOfTapsRequired = 2
collectionView.addGestureRecognizer(tapGesture)

셀 및 뷰 조작하기

만들기로 계획한 조작의 타입에 따라 셀과 뷰를 조작하기 위해 어떻게 제스처 레코그나이저를 사용해야 하는가? 간단한 삽입 및 삭제는 표준 제스처 레코그나이저의 액션 메소드 내에서 수행될 수 있다. 하지만 더 복잡한 조작을 계획하고 있다면 터치 이벤트를 추적하기 위해 커스텀 제스처 레코그나이저를 정의할 필요가 있을 수 있다.

조작의 한 종류로 컬렉션 뷰에의 한 위치에서 다른 위치로 이동하는 셀을 위한 커스텀 제스처 레코그나이저를 필요로 할 수 있다. 셀을 옮기는 가장 직관적인 방법은 (일시적으로) 그 셀을 컬렉션 뷰에서 삭제하고, 그 셀에 대한 시각적 표현을 드래그하고, 터치 이벤트가 끝날 때 새로운 위치에 셀을 추가하는 것이다. 이 모든 것은 직접 터치 이벤트들을 직접 관리하는 것을 요구하며, 새로운 삽입 위치를 결정하기 위해 레이아웃 객체와 밀접하게 작업하며 데이터 소스의 변화를 조작하고 새로운 위치에 아이템을 삽입하는 것을 관리해야 한다.

Event Handling Guide for iOS에서 커스텀 제스처 레코그나이저를 만드는 것에 대한 더 많은 정보를 확인하라.

커스텀 레이아웃 만들기

커스텀 레이아웃을 만들기 전에 이것이 정말로 필요한 것인지 고려하라. UICollectionViewFlowLayout 클래스는 효율성을 위해 이미 최적화된 상당한 양의 동작을 제공하고 많은 다른 종류의 표준 레이아웃을 얻기 위해 몇 가지 방법으로 적응될 수 있다. 커스텀 레이아웃을 구현하기를 고려할 수 있는 유일한 시점은 다음의 상황 뿐이다.

  • 원하는 레이아웃은 격자 또는 라인 기반 분리 레이아웃 (아이템이 하나의 행이 가득 찰 때까지 놓여지며, 모든 아이템이 놓여질 때까지 다음 라인에서 지속하는 것) 처럼 보이지 않거나 반드시 하나 이상의 방향으로 스크롤될 수 있어야 한다.
  • 모든 셀의 위치를 주기적으로 변경하고 싶으며, 커스텀 레이아웃을 만드는 것보다 기존의 플로우 레이아웃을 수정하여 충분히 달성할 수 있다.

API 관점에서 커스텀 레이아웃을 구현하는 것은 어렵지 않다. 가장 어려운 부분은 레이아웃에서 아이템의 위치를 결정하기 위해 필요한 연산을 수행하는 것이다. 그러한 아이템들의 위치를 알고 있다면, 컬렉션 뷰에 그 정보를 제공하는 것은 간단하다.

UICollectionViewLayout 서브클래싱하기

커스텀 레이아웃을 위해 UICollectionViewLayout를 서브클래싱하기 원한다. 이는 디자인을 위한 새로운 시작점을 제공한다. 오직 몇 개의 메소드만이 레이아웃 객체에 대한 핵심 동작을 제공하며 구현해야 할 필요가 있다. 나머지 메소드들은 레이아웃 동작을 비틀기 위해 필요로 할 때 재정의할 수 있다. 핵심 메소드는 다음의 중요한 작업을 처리한다.

  • 스크롤 가능한 컨텐츠 영역의 크기를 지정한다.
  • 레이아웃을 만드는 셀과 뷰에 대한 애트리뷰트 객체를 제공하여 컬렉션 뷰가 각 셀과 뷰의 위치를 설정할 수 있도록 한다.

오직 핵심 메소드만을 구현한 기능적인 레이아웃 객체를 만들 수 있을지라도, 몇 가지 선택적인 메소드도 구현한다면 더욱 매력적이게 될 수 있다.

레이아웃 객체는 컬렉션 뷰의 레이아웃을 만들기 위해 데이터 소스가 제공한 정보를 사용한다. 레이아웃은 collectionView 프로퍼티에 대한 메소드를 호출하여 데이터 소스와 소통한다. 모든 레이아웃의 메소드 내에서 이 프로퍼티에 접근 가능하다. 컬렉션 뷰가 레이아웃 프로세스 동안 컬렉션 뷰가 알고 있고 알지 못하고 있는 것을 염두에 두어라. 레이아웃 프로세스는 이미 시작되어 컬렉션 뷰는 뷰의 레이아웃이나 배치를 추적할 수 없다. 그러므로 레이아웃 객체가 컬렉션 뷰의 어떠한 메소드를 호출하는 것을 제한하지 않을 것이지만, 레이아웃을 계산하는 데 필수적인 데이터 이외의 다른 것에 대해서는 컬렉션 뷰에 의존하지 않도록 하라.

핵심 레이아웃 프로세스 이해하기

컬렉션 뷰는 전체 레이아웃 프로세스를 관리하기 위해 커스텀 레이아웃 객체와 직접적으로 작동한다. 컬렉션 뷰가 레이아웃 정보를 필요로 한다고 결정할 때 레이아웃 객체에게 그것을 제공하라고 요청한다. 예를 들어 컬렉션 뷰는 최초로 표시되거나 사이즈가 조절될 때 레이아웃 정보를 요청한다. 레이아웃 객체의 invalidateLayout() 메소드를 호출하여 명시적으로 컬렉션 뷰에게 레이아웃을 갱신하라고 말할 수 있다. 이 메소드는 존재하는 레이아웃 정보를 모두 날려버리고 새로운 레이아웃 정보를 만드라고 레이아웃 객체에게 강제한다.

알아두기 : 컬렉션 뷰의 reloadData() 메소드와 레이아웃 객체의 invalidateLayout() 메소드를 혼동하지 말도록 주의하라. invalidateLayout() 메소드를 호출하는 것은 컬렉션 뷰가 존재하는 셀과 서브뷰를 모두 날려버리는 것을 반드시 야기하지 않는다. 대신에 아이템을 이동하고 추가하거나 삭제할 때 필요하다면 레이아웃 객체가 모든 레이아웃 애트리뷰트를 다시 계산하도록 강제한다. 데이터 소스에 있는 데이터가 변경된다면 reloadData() 메소드가 적합하다. 어떻게 레이아웃 갱신을 개시하는지에 상관 없이, 실제 레이아웃 프로세스는 동일하다.

레이아웃 프로세스 동안 컬렉션 뷰는 레이아웃 객체의 특정 메소드를 호출한다. 이 메소드들은 아이템의 위치를 계산하고 컬렉션 뷰에게 필요한 중요한 정보를 제공하는 기회를 준다. 또한 다른 메소드들도 호출될 수 있으나, 이 메소드들은 다음의 순서에 따른 레이아웃 프로세스 동안에 항상 호출된다.

  1. prepare() 메소드를 사용하여 레이아웃 정보를 제공하기 위해 필요한 사전 계산을 수행한다.
  2. collectionViewContentSize 프로퍼티를 사용하여 초기 계산에 기반한 전체 컨텐츠 영역의 전체 크기를 반환한다.
  3. layoutAttributesForElements(in:) 메소드를 사용하여 특정 직사각형에 있는 셀과 뷰의 애트리뷰트를 반환한다.

prepare() 메소드는 레이아웃에서 셀과 뷰의 위치를 결정하기 위해 필요한 어떠한 연산이든 수행할 수 있는 기회를 준다. 최소한 이 메소드가 2단계에서 컬렉션 뷰에게 반환할 전체 컨텐츠 영역의 크기를 반환할 수 있을 만큼 충분한 정보를 계산해야 한다.

컬렉션 뷰는 스크롤 뷰를 적절하게 구성하기 위해 컨텐츠 크기를 사용한다. 예를 들어 계산된 컨텐츠 크기가 수직 및 수평 방향 모두로 현재 디바이스의 화면 경계를 벗어나서 확대된다면, 스크롤 뷰는 동시에 두 개의 방향에 대하여 스크롤이 가능하도록 조절한다. UICollectionViewFlowLayout과는 다르게 이는 기본적으로 오직 한 방향으로만 스크롤 가능하도록 컨텐츠의 레이아웃을 조절하지 않는다.

현재 스크롤 위치에 기반하여, 컬렉션 뷰는 layoutAttributesForElements(in:) 메소드를 호출하여 특정 직사각형 내의 셀과 뷰의 특성들을 요청하며, 직사각형은 보여지고 있을 수도, 보여지기 않고 있을 수도 있다. 이 정보를 반환한 후 핵심 레이아웃 프로세스는 유효하게 완료된다.

레이아웃이 종료한 후 셀과 뷰의 애트리뷰트는 컬렉션 뷰가 레이아웃을 무효화할 때까지 동일하게 남아 있는다. 레이아웃 객체의 invalidateLayout() 메소드를 호출하여, prepare() 메소드를 새로 호출하는 것부터 시작하는 레이아웃 프로세스가 다시 시작하도록 할 수 있다. 컬렉션 뷰는 또한 스크롤하는 동안 자동으로 레이아웃을 무효화할 수 있다. 사용자가 컨텐츠를 스크롤하면 컬렉션 뷰는 레이아웃 객체의 shouldInvalidateLayout(forBoundsChange:) 메소드를 호출하고 이 메소드가 true를 반환한다면 레이아웃을 무효화한다.

알아두기 : invalidateLayout() 메소드가 즉시 레이아웃 갱신 프로세스를 시작하지 않는다는 것을 기억해 두면 좋다. 이 메소드는 레이아웃이 데이터와 일관되지 않고 갱신이 필요하다고 표시한다. 다음 뷰 갱신 사이클 동안 컬렉션 뷰는 레이아웃이 더럽혀졌는지, 그렇다면 갱신해야 하는지 확인한다. 사실 매 시점에 즉각적인 레이아웃 갱신을 트리거하지 않고도 빠르게 연속하여 여러 번 invalidateLayout() 메소드를 호출할 수 있다.

레이아웃 애트리뷰트 만들기

레이아웃이 담당하는 애트리뷰트 객체는 UICollectionViewLayoutAttributes 클래스의 인스턴스다. 이 인스턴스들은 다양한 다른 메소드들에서 만들어질 수 있다. 수천 개의 아이템을 다루지 않는다면 레이아웃을 준비하는 동안 이 인스턴스들을 만드는 게 맞을 것이다. 레이아웃 정보는 계산된 후 날라가 버리는 것보다는 캐시되어 참조될 수 있기 때문이다. 모든 애트리뷰트를 계산하는 데 드는 컴퓨팅 비용이 캐싱을 하는 것의 이점보다 큰 경우, 요청받은 순간에 애트리뷰트를 만드는 것이 쉽다.

아무튼, UICollectionViewLayoutAttributes 클래스의 새로운 인스턴스를 만들 때 다음의 이니셜라이저 중 하나를 사용한다.

  • init(forCellWith:)
  • init(forSupplementaryViewOfKind:with:)
  • init(forDecorationViewOfKind:with:)

컬렉션 뷰는 데이터 소스 객체로부터 적절한 타입의 뷰를 요청하기 위해 그 정보를 사용하므로, 반드시 표시되려고 하는 뷰의 타입에 기반하여 올바른 이니셜라이저를 사용해야 한다. 올바르지 않은 이니셜라이저를 사용하면 컬렉션 뷰가 잘못된 위치에 잘못된 뷰를 만들도록 할 수 있고 레이아웃은 의도한 대로 보이지 않게 된다.

각각의 애트리뷰트 객체를 만든 후 부합하는 뷰와 관련된 특성을 설정하라. 최소한 레이아웃에 있는 뷰의 크기와 위치는 설정하라. 레이아웃에 있는 뷰가 겹쳐지는 경우 겹쳐지는 뷰들의 일관된 순서를 보장하기 위해 zIndex 프로퍼티에 값을 할당하라. 다른 프로퍼티들은 셀이나 뷰의 가시성 또는 외관을 제어하고, 필요하다면 변화될 수 있게 해준다. 표준 애트리뷰트 클래스가 니즈에 맞지 않다면 서브클래싱하여 각각의 뷰에 대한 다른 정보를 저장할 수 있도록 확장할 수 있다. 레이아웃 애트리뷰트를 서브클래싱할 때 isEqual(to:) 메소드를 구현하여 커스텀 애트리뷰트를 비교할 수 있게 해야 한다. 컬렉션 뷰는 연산의 일부분에 해당 메소드를 사용한다.

UICollectionViewLayoutAttributes Clasas Reference에서 레이아웃 애트리뷰트에 관한 더 많은 정보를 확인하라.

레이아웃 준비하기

레이아웃 사이클의 시작점에서 레이아웃 객체는 레이아웃 프로세스를 시작하기 전 prepare()를 호출한다. 이 메소드는 나중에 레이아웃에게 알려줄 정보를 계산할 기회를 제공한다. prepare() 메소드는 커스텀 레이아웃을 구현하기 위해 반드시 필요한 것은 아니지만 필요하다면 초기 계산을 할 기회를 제공하기 위해 제공된다. 이 메소드가 호출된 후 레이아웃은 반드시 컬렉션 뷰의 컨텐츠 크기를 계산하기에 충분한 정보를 가지고 있어야 한다. 이는 레이아웃 프로세스의 다음 단계다. 하지만 해당 정보는 이 최소 요구사항부터 레이아웃이 사용할 모든 레이아웃 애트리뷰트 객체의 생성과 저장에 이르기까지 다양하다. prepare() 메소드를 사용은 인프라의 대상이며 사전에 계산하는 게 맞는지, 요청이 있어야 계산하는 것이 맞는지의 대상이다. Prepraing the Layout에서 prepare() 메소드가 어떻게 생겼는지에 대한 예제를 확인하라.

주어진 직사각형 내의 아이템에 대한 레이아웃 애트리뷰트 제공하기

레이아웃 프로세스의 최종 단계 동안 컬렉션 뷰는 레이아웃 객체의 layoutAttributesForElements(in:) 메소드를 호출한다. 이 메소드의 목적은 지정된 직사각형과 교차하는 모든 셀, 모든 서플먼터리 뷰, 모든 데코레이션 뷰에 대한 레이아웃 애트리뷰트를 제공하는 것이다. 거대한 스크롤 가능한 컨텐츠 영역에 대하여, 컬렉션 뷰는 현재 보여지고 있는 컨텐츠 영역의 일부분에 대한 아이템의 애트리뷰트를 요청할 수 있다. 반드시 컬렉션 뷰 컨텐츠 영역의 어떠한 부분에 대한 레이아웃 애트리뷰트를 제공하기 위해 준비되어 있어야 한다. 그러한 특성은 삽입 또는 삭제되는 아이템에 대한 애니메이션을 촉진하기 위해 사용될 수 있다.

layoutAttributesForElements(in:) 메소드는 레이아웃 객체의 prepare() 메소드 이후에 호출되기 때문에, 이미 요구하는 애트리뷰트를 반환하거나 만들기 위해 필요한 대부분의 정보를 가지고 있어야 한다. layoutAttributesForElements(in:) 메소드의 구현은 다음의 단계를 따른다.

  1. prepare() 메소드에서 만들어낸 데이터를 순회하여 캐시된 특성이나 새로 생성된 애트리뷰트에 접근한다.
  2. 각 아이템의 프레임이 layoutAttributesForElements(in:) 메소드에 넘어간 직사각형과 교차하는지 확인한다.
  3. 교차하는 각 아이템에 대하여 배열에 부합하는 UICollectionViewLayoutAttributes 객체를 추가한다.
  4. 컬렉션 뷰에 레이아웃 애트리뷰트 배열을 반환한다.

레이아웃 정보를 관리하는 방법에 따라 prepare() 메소드에서 UICollectionViewLayoutAttributes 객체를 만들거나, 기다린 후 layoutAttribtuesForElements(_:) 메소드에서 그 작업을 수행할 수 있다. 요구에 맞는 구현을 하는 동안, 레이아웃 정보를 캐싱하는 것에 대한 이점을 염두에 두어라. 셀에 대하여 새로운 레이아웃 애트리뷰트를 반복적으로 계산하는 것은 비싼 작업이며, 성능에 눈에 띄게 좋지 않은 영향을 줄 수 있다. 그렇긴 하지만 컬렉션 뷰가 관리하는 아이템의 양이 크다면 (성능을 위해) 요청될 때 레이아웃 애트리뷰트를 만드는 것이 더 말이 될 수 있다. 이는 어떠한 올바른 전략을 사용하는지에 관한 문제일 뿐이다.

알아두기 : 레이아웃 객체는 또한 개별 아이템에 대해 필요할 때 레이아웃 애트리뷰트를 제공할 수 있을 필요가 있다. 컬렉션 뷰는 적절한 애니메이션을 만드는 것을 포함한 몇 가지 이유로 일반적인 레이아웃 프로세스의 바깥에서 그 정보를 요청할 수 있다. Providing Layout Attributes On Demand에서 필요 시 레이아웃 애트리뷰트를 제공하는 것에 관한 더 많은 정보를 확인하라.

Providing Layout Attributes에서 layoutAttributesForElements(in:)의 구현에 관한 예시를 확인하라.

필요할 때 레이아웃 애트리뷰트 제공하기

컬렉션 뷰는 주기적으로 레이아웃 객체에게 공식적인 레이아웃 프로세스의 바깥에서 개별 아이템에 대한 애트리뷰트를 제공하라고 요청한다. 예를 들어 컬렉션 뷰는 아이템에 대한 삽입 및 삭제 애니메이션을 구성할 때 이 정보를 요청한다. 레이아웃 객체는 각각의 셀, 서플먼터리 뷰, 그리고 지원하는 데코레이션 뷰에 대한 레이아웃 애트리뷰트를 제공하기 위해 반드시 준비되어 있어야 한다.

  • layoutAttributesForItem(at:)
  • layoutAttributesForSupplementaryView(ofKind:at:)
  • layoutAttributesForDecorationView(ofKind:at:)

이 메소드들의 구현은 주어진 셀이나 뷰에 대한 현재 레이아웃 애트리뷰트를 가져와야 한다. 모든 커스텀 레이아웃 객체는 layoutAttributesForItem(at:) 메소드를 구현하기를 기대한다. 레이아웃이 서플먼터리 뷰를 포함하지 않는다면 layoutAttributesForSupplementaryView(ofKind:at:) 메소드를 재정의할 필요가 없다. 비슷하게 데코레이션 뷰를 포함하지 않는다면 layoutAttributesForDecorationView(ofKind:at:) 메소드를 재정의할 필요가 없다. 애트리뷰트를 반환할 때 레이아웃 애트리뷰트를 갱신하면 안된다. 레이아웃 정보를 변경할 필요가 있다면, 레이아웃 객체를 무효화하고 이어지는 레이아웃 사이클에서 해당 데이터를 갱신하도록 해야 한다.

커스텀 레이아웃을 사용하기 위해 연결하기

컬렉션 뷰에 커스텀 레이아웃을 연결하는 두 가지 방법이 있다. 코드 또는 스토리보드를 통해서 할 수 있다. 컬렉션 뷰는 쓰기 가능한 프로퍼티, collectionViewLayout을 통해 레이아웃을 연결한다. 커스텀 구현에서 레이아웃을 설정하기 위해 컬렉션 뷰의 레이아웃 프로퍼티를 커스텀 레이아웃 객체의 인스턴스로 설정하라.

collectionView.collectionViewLayout = MyCustomLayout()

그렇지 않으면, 스토리보드에서 Document Outline 패널을 열고 컬렉션 뷰를 선택하라. (컨트롤러를 위한 드랍다운 메뉴 목록에 있다.) 컬렉션 뷰가 선택되면 Utilities 페인에서 Attributes Inspector를 열고 Collection View로 라벨링된 섹션 아래에서 Layout 옵션을 Flow에서 Custom으로 변경하라. 이것 아래에 있는 옵션은 Scroll Direction에서 Class로 변경되며, 이제 커스텀 레이아웃 클래스를 선택할 수 있다.

커스텀 레이아웃을 더욱 매력적이게 만들기

레이아웃 프로세스 동안 각각의 셀과 뷰에 대한 레이아웃 애트리뷰트를 제공하는 것은 필수적이다. 하지만 커스텀 레이아웃에서 사용자 경험을 향상시킬 수 있는 다른 동작도 있다. 이 동작을 구현하는 것은 선택적이나 추천된다.

서플먼터리 뷰를 통해 컨텐츠를 고양시키기

서플먼터리 뷰는 컬렉션 뷰 셀과 분리되어 그 자신만의 레이아웃 애트리뷰트 집합을 갖는다. 셀처럼, 이 뷰들은 데이터 소스 객체가 제공하지만, 메인 컨텐츠를 향상시키는 것이 그것들의 목적이다. 예를 들어 UICollectionViewFlowLayout은 서플먼터리 뷰를 섹션 헤더와 푸터로 사용한다. 서플먼터리 뷰가 각 셀에게 그 자신만의 텍스트 레이블을 가져 셀에 대한 정보를 표시할 수 있도록 할 수 있다. 컬렉션 뷰 셀처럼, 서플먼터리 뷰는 컬렉션 뷰가 사용하는 리소스의 양을 최적화하기 위해 재활용 프로세스를 겪는다. 그러므로 사용되는 모든 서플먼터리 뷰는 UICollectionViewReusableView 클래스의 서브클래스가 되어야 한다.

레이아웃에 서플먼터리 뷰를 추가하는 단계는 다음을 따른다.

  1. register(_:forSupplementaryViewOfKind:withReuseIdentifier:) 메소드를 사용하여 컬렉션 뷰의 레이아웃 객체에 서플먼터리 뷰를 등록한다.
  2. 데이터 소스에서 collectionView(_:viewForSupplementaryElementOfKind:at:)를 구현한다. 이 뷰들은 재사용 가능하기 때문에 dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:) 메소드를 호출하여 디큐하거나 새로운 재사용 가능한 뷰를 만들고 반환하기 전에 필요로 하는 데이터를 설정한다.
  3. 셀에서 한 것처럼 서플먼터리 뷰에 대한 레이아웃 애트리뷰트 객체를 만든다.
  4. 이 레이아웃 애트리뷰트 객체들을 layoutAttributesForElements(in:) 메소드가 반환하는 애트리뷰트 배열에 포함시킨다.
  5. layoutAttributesForSupplementaryView(ofKind:at:) 메소드를 구현하여 질의될 때마다 특정 서플먼터리 뷰에 대한 애트리뷰트 객체를 반환한다.

커스텀 레이아웃에서 서플먼터리 뷰를 위한 애트리뷰트 객체를 만드는 프로세스는 셀을 위한 프로세스와 거의 동일하지만, 커스텀 레이아웃은 여러 타입의 서플먼터리 뷰를 가질 수 있는 반면 셀은 한 가지 타입으로 제한된다는 차이가 있다. 서플먼터리 뷰는 메인 컨텐츠를 향상시킨다는 의미가 있고, 그러므로 메인 컨텐츠와 분리되어야 하기 때문이다. 컨텐츠를 보충할 수 있는 많은 방법이 있고, 그러므로 각 서플먼터리 뷰의 메소드는 다른 것들과 구별하기 위해 뷰의 종류를 전달하라고 명시하며, 레이아웃 객체는 그 타입에 기반하여 올바르게 애트리뷰트를 계산할 수 있게 한다. 사용하기 위해 서플먼터리 뷰를 등록할 때, 제공하는 문자열은 다른 것들과 뷰를 구별하기 위해 레이아웃 객체가 상용한다. Incorporating Supplementary Views에서 커스텀 레이아웃에 서플먼터리 뷰를 통합하는 예제를 확인하라.

커스텀 레이아웃에 데코레이션 뷰 포함시키기

데코레이션 뷰는 컬렉션 뷰 레이아웃의 외관을 향상시키기 위한 시각적 장식이다. 셀과 서플먼터리 뷰와는 다르게, 데코레이션 뷰는 오직 시각적 컨텐츠만을 제공하고, 그러므로 데이터 소스와 독립되어 있다. 커스텀 배경을 제공하고, 셀 주변 공간을 채우고, 원한다면 셀을 흐릿하게 만들기 위해 사용할 수 있다. 데코레이션 뷰는 레이아웃 객체가 단독적으로 정의하고 관리하며 컬렉션 뷰의 데이터 소스 객체와 상호 작용하지 않는다.

데코레이션 뷰를 레이아웃에 추가하기 위해 다음을 따르라.

  1. 레이아웃 객체에 register(_:forDecorationViewOfKind:) 메소드를 사용하여 데코레이션 뷰를 등록한다. 이 접근법이 셀과 서플먼터리 뷰를 등록하는 것과 닮아 있지만, 데코레이션 뷰를 등록하는 것은 데이터 소스 내부가 아닌, 레이아웃 객체 내부에서 일어나는 것임을 기억하라.
  2. 레이아웃 객체의 layoutAttribtuesForElements(in:) 메소드에서 셀과 서플먼터리 뷰에 대해서 한 것처럼 데코레이션 뷰에 대한 애트리뷰트를 만든다.
  3. 레이아웃 객체에 layoutAttributesForDecorationView(ofKind:at:) 메소드를 구현하여 요청될 때 데코레이션 뷰에 대한 애트리뷰트를 반환한다.
  4. 선택적으로 initialLayoutAttributesForAppearingDecorationElement(ofKind:at:)finalLayoutAttributesForDisappearingDecorationElement(ofKind:at:) 메소드를 구현하여 데코레이션 뷰의 보여짐과 사라짐에 대한 애니메이션을 처리한다. Making Insertion and Deletion Animations More Interesting에서 더 많은 정보를 확인하라.

데코레이션 뷰의 생성 프로세스는 셀과 서플먼터리 뷰의 프로세스와 다르다. 클래스나 xib 파일을 등록하는 것은 데코레이션 뷰가 필요할 때 만들어지는 것을 보장하기 위해 해야 하는 전부다. 데코레이션 뷰는 순전히 시각적인 것이므로 제공된 xib 파일이나 객체의 init(frame:) 이니셜라이저에서 하는 것 이상의 구성이 필요하다고 기대하지 않는다. 이러한 이유로 데코레이션 뷰가 필요하면 컬렉션 뷰는 그것을 만들고 레이아웃 객체에 제공한 애트리뷰트를 적용한다. 데코레이션 뷰는 여전히 UICollectionReusableView의 서브클래스여야 한다. 레이아웃 객체가 데코레이션 뷰에 재활용 매커니즘을 적용하기 때문이다.

알아두기 : 데코레이션 뷰에 대한 특성을 만들 때 zIndex 프로퍼티를 고려하는 것을 잊지 마라. 데코레이션 뷰를 표시되는 셀과 서플먼터리 뷰의 뒤 (또는, 선호한다면 앞) 에 배치하기 위해 zIndex 특성을 사용할 수 있다.

삽입 및 삭제 애니메이션을 더욱 흥미롭게 만들기

셀과 뷰를 삽입하고 삭제하는 것은 레이아웃 동안 흥미로운 도전이 된다. 셀을 삽입하는 것은 다른 셀과 뷰에 대한 레이아웃이 변경되는 원인이 될 수 있다. 레이아웃 객체가 현재 위치에서 새로운 위치로 존재하는 셀과 뷰를 애니메이팅하는 방법을 안다고 할지라도, 삽입되려는 셀에 대한 현재 위치에 대한 정보는 없다. 애니메이션 없이 새로운 셀을 삽입하기보다는, 컬렉션 뷰는 레이아웃 객체에게 애니메이션을 위해 사용할 초기 애트리뷰트 집합을 제공하라고 요청한다. 비슷하게, 셀이 삭제될 때 컬렉션 뷰는 레이아웃 객체에게 애니메이션의 끝을 위해 사용할 최종 애트리뷰트 집합을 제공하라고 요청한다.

초기 특성이 작동하는 방법에 대해 이해하기 위해 예제를 보는 것이 좋겠다. 시작 레이아웃은 컬렉션 뷰가 초기에 오직 세 개의 셀만을 포함하고 있다는 것을 보여준다. 새로운 셀이 삽입될 때 컬렉션 뷰는 삽입되려는 셀에 대한 초기 애트리뷰트를 제공하라고 레이아웃 객체에게 요청한다. 이 경우 레이아웃 객체는 셀의 초기 위치를 컬렉션 뷰의 중앙으로 설정하고 알파 값을 0으로 설정하여 숨긴다. 애니메이션 동안 새로운 셀은 페이드인 하면서 나타나고 컬렉션 뷰의 중앙에서 우측 하단 코너에 있는 최종 위치로 이동한다.

다음은 삽입되는 셀에 대한 초기 애트리뷰트를 지정하기 위해 사용되는 코드다. 이 메소드는 셀의 위치를 컬렉션 뷰의 중앙으로 설정하고 투명하게 만든다. 레이아웃 객체는 정상적인 레이아웃 프로세스의 일부분으로서 셀의 최종 위치와 알파 값을 제공한다.

func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let attributes = layoutAttributesForItem(at: itemIndexPath)
attributes.alpha = 0

let size = collectionView.frame.size
attributes.center = .init(width: size.width / 2, height: size.height / 2)

return attributes
}

삭제를 처리하는 프로세스는 초기 애트리뷰트 대신 최종 애트리뷰트를 지정해야 한다는 것을 제외하고 삽입 프로세스와 동일하다. 이전의 예제에서 셀을 삽입할 때 사용했던 특성과 동일한 특성을 사용한다면 셀을 삭제하는 것은 컬렉션 뷰의 중앙으로 이동하면서 페이드 아웃 되는 원인이 될 것이다. UICollectionViewLayout에는 사용할 수 있는 여섯 개의 메소드가 있다. 아이템, 서플먼터리 뷰, 데코레이션 뷰를 위한 두 개의 분리된 메소드 (초기 애트리뷰트를 위한 것 / 최종 애트리뷰트를 위한 것) 가 있다.

레이아웃의 스크롤 경험을 향상시키기

커스텀 레이아웃 객체는 더 나은 사용자 경험을 만들기 위해 컬렉션 뷰의 스크롤 동작에 영향을 줄 수 있다. 스크롤과 관련된 터치 이벤트가 끝날 때 스크롤 뷰는 현재 속도와 감속 비율에 기반하여 스크롤 중인 컨텐츠가 최종적으로 놓여질 위치를 결정한다. 컬렉션 뷰가 그 위치를 알게 될 때 레이아웃 객체에게 그 위치가 targetContentOffset(forProposedContentOffset:withScrollingVelocity:) 메소드를 호출하여 수정 가능한지 묻는다. 밑에 있는 컨텐츠가 여전히 움직이고 있을 때 이 메소드를 호출하기 때문에 커스텀 레이아웃은 스크롤되고 있는 컨텐츠가 최종적으로 놓여질 포인트에 영향을 미칠 수 있다.

컬렉션 뷰의 오프셋이 (0, 0)에서 시작하고 사용자가 왼쪽으로 스와이프한다고 가정하자. 컬렉션 뷰는 어디에서 스크롤이 자연적으로 멈출지를 계산하고 그 값을 “제안된” 컨텐츠 오프셋 값으로 제공한다. 레이아웃 객체는 스크롤이 멈출 때, 아이템이 컬렉션 뷰의 보여지는 바운드 내에서 정확히 중앙에 위치하도록 하기 위해 제안된 값을 변경할 수 있다. 이 새로운 값은 타겟 컨텐츠 오프셋이 되고 targetContentOffset(forProposedContentOffset:withScrollingVelocity:) 메소드가 반환할 값이 된다.

커스텀 레이아웃을 구현하기 위한 팁

커스텀 레이아웃 객체를 구현하기 위한 몇 가지 팁과 제안이다.

  • prepare() 메소드를 사용하여 나중에 필요로 할 UICollectionViewLayoutAttributes 객체들을 만들고 저장하는 것을 고려하라. 컬렉션 뷰는 몇몇 지점에서 레이아웃 애트리뷰트 객체들을 요청할 것이므로, 몇 가지 경우에 미리 만들고 저장하는 것이 말이 된다. 특히 상대적으로 적은 개수의 아이템 (몇 백 개) 을 가지고 있거나 그러한 아이템에 대한 실제 레이아웃 특성이 드물게 변경된다면 정말로 그렇다.

    그럼에도 불구하고 레이아웃이 수 천 개의 아이템을 관리할 필요가 있다면, 캐싱과 다시 계산하는 것 간의 이점을 비교해볼 필요가 있다. 레이아웃이 드물게 변경되는 변동이 심한 아이템의 경우 대개 캐싱은 주기적으로 복잡한 레이아웃을 다시 계산할 필요를 없앤다. 큰 숫자의 고정된 크기라면 필요할 때 애트리뷰트를 계산하는 것이 더 간단할 수 있다. 주기적으로 변화하는 애트리뷰트를 가진 아이템의 경우 아무튼 매번 다시 계산해야 할 것이므로 캐싱은 메모리에서 여분의 공간을 차지하게 될 뿐이다.

  • UICollectionView를 서브클래싱하지 않도록 하라. 컬렉션 뷰는 그 자체만으로는 외관상으로 가지고 있는 게 적다. 대신 데이터 소스 객체로부터 모든 뷰를 끌어오고 레이아웃 객체로부터 레이아웃과 관련된 모든 정보를 끌어온다. 3차원에서 아이템들을 배치하려고 한다면 각 셀과 뷰에 대해 3차원 변환을 적절하게 설정하는 커스텀 레이아웃을 구현하는 것이 적절한 방법이다.

  • 커스텀 레이아웃 객체의 layoutAttributesForElements(in:) 메소드에서 UICollectionViewvisibleCells 프로퍼티를 절대로 호출하지 마라. 컬렉션 뷰는 아이템이 어디에 위치하는지에 대한 정보를 알지 못한다. 그것은 레이아웃 객체가 알려주는 것이다. 그러므로 보여지고 있는 셀을 요청하는 것은 레이아웃 객체로 전달된다.

    레이아웃 객체는 항상 컨텐츠 영역에서 아이템들의 위치를 알고 있어야 하고, 언제나 그러한 아이템들의 특성을 반환할 수 있어야 한다. 대부분의 경우 자체적으로 이를 해내야 한다. 몇 안되는 경우에 레이아웃 객체는 아이템을 배치하기 위해 데이터 소스에 있는 정보에 의존할 수 있다. 예를 들어 지도에 아이템을 표시하는 레이아웃은 데이터 소스에서 각 아이템의 지도 위치를 받아올 것이다.