CollectionView -Compositional Layout

Astrid
16 min readAug 12, 2021

有兩種方式可以製作CollectionView,比較舊的(ios13以前)是flow layout,比較新的(ios13之後)是compositional layout。 flow layout可以用builder interface設置,而compositional layout一定要用code設置。

這篇來筆記要怎麼用compositional layout製作CollectionView。

在實際操作以前,要先了解一下collection view的名詞(Item, Group, Section)。

Item: data的基本單位。

Group: layout的基本單位。決定data呈現的方向,多個Group組合可以做出更複雜的layout。

Section:Section is simply a grouping of data and corresponds to how the data is organized in the data source.一個collectionView可以有多個sections,每個section裡有他自己的groups與items。

Items,Groups,Sections可以製作出Compositional layout object。

以下紅色框是Section。Section中有兩個Group,一個Group是藍色框的Vertical Group,裡面有3個Item。另一個Group是下下面gif的Horizontal Group。

Horizontal Group

以下是簡略的重點步驟:

Step1. 設置CollectionView的格式(section, item, group 這三個一定要設定。)

Step2. 設置CollectionView的顯示資料內容(dataSource)。

Step3.設置Snapshot讓以上設定好的CollectionView顯示出來。

Step4.在viewDidLoad中執行Step2–4的方法。

接下來就來詳細實做吧!

*將collection view抓進viewController。

並用autoLayout四邊皆為0讓collection 跟 view一樣大。

接著control + drag collection view到 ViewController。

Step1. 設置CollectionView的格式(section, item, group 這三個一定要設定。)

設置的方式是,寫一個會回傳UICollectionViewCompositionalLayout的method,在此method內設定section, item, group。

func configureLayout() -> UICollectionViewCompositionalLayout

接著再把這個回傳值assign給collection view的collectionViewLayout屬性。

collectionView.collectionViewLayout = configureLayout()

method中設置時的優先順序是item → group → section。

Item:

設置item,就是實例化NSCollectionLayoutItem這個class。這個被實例化的item並不是真正顯示在畫面上的,它其實是一個藍圖,當真正有data提供時,就知道要如何顯示。

實例化NSCollectionLayoutItem這個class時,需要提供size,size的型別是NSCollectionLayoutSize,而這又是由NSCollectionLayoutDimension的instances組成的。

NSCollectionLayoutDimension有四種屬性。

absolute:這裡寫多少,就會顯示多少長或寬。

estimated:可以讓item根據他內含的內容調整大小。

fractionlHeight:此item跟它parent container(也就是包著item的group)的Height的比例(0到1之間)。

fractionlWidth:此item跟它parent container(也就是包著item的group)的Width的比例(0到1之間)。

做好了item

Group:

設置group,就是實例化NSCollectionLayoutGroup這個class。

Group is a flexible container to contain Item.

實例化NSCollectionLayoutGroup這個class時,需要選擇是horizontal還是vertical;也需標明此Group的內容(subItem)是什麼 ; 需要提供size,size的型別同樣是NSCollectionLayoutSize

做好了group

Section:

Section的設置比較簡單,他的預設寬會跟整個螢幕一樣寬,他的高取決於他的內容(也就是他裝著的group)。所以只需要標明Section內裝著的group即可。

做好了group

item, group, section都設置好了之後,就可以return UICollectionViewCompositionalLayout

以下是目前,設置好layout的狀態。接下來要處理顯示資料內容。

layout設置完成

*在storyBoard客製化cell。

collectionView沒有預設好的cell,所以都要自己做。

先把cell的長寬拉出來,在這裡拉的話,還是會被在code裡的設定覆蓋掉,但在這裡做出來有助於我們知道自己在做什麼。

拉出cell長寬。

接著加入label,然後給這個cell一個reuse identifier。

設定reuse identifier

*新增一個class專門用來掌管這個cell。

在NumberCell class中,創建reuse idenfifier的屬性。

static let reuseIdenfifier = String(describing: NumberCell.self)

以上程式碼會產生NumberCell class的名字的字串。

接著回到storyBoard,把剛剛的cell的class改成新建好的NumberCell class。

並且把label跟code連起來。

Step2. 設置CollectionView的顯示資料內容(dataSource)。

collection view 的 格式 跟顯示資料 分別由不同的object負責,而不是collection view自己掌管這一切。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

— — — — — — — — -Collection View Layout Object — — — — — — — — — —

— — — — — — — — — — — — ↗️ — — — — — — — — — — — — — — — — — —

Collection View Responsibility

— — — — — — — — — — — — ↘️ — — — — — — — — — — — — — — — — — —

— — — — — — — — — — — -Data Source Object: — — — — — — — — — — — — — — — — — — — — — — — — Manage Data — — — — — — — — — — — — — — — — — — — — — — -Provides collection view with snapshots to display.

— — — — — — — — — — - — — — — — — — — — — - — — — — — — — — — — -

當 Data Source Object要顯示資料時,我們首先要定義initial snapshot。

snapshot是the truth of the current UI state。我們提供給Data Source Object最開始的snapshot,並也跟他說要如何處理data。

接著Data Source Object會去跟collection view要cell,並把剛剛交代給他的事apply到collection view。除此之外,Data Source Object還能做更進階的事。

Diffable Data Sources:

假設initial snapshot 是一串名單,後來我們想要將同樣的名單改成按照字母排序(這就是我們提供的second snapshot)。Data Source Object可以判別initial snapshot與second snapshot之間的差異(diff),然後告訴collection view要如何移動目前的資料,而不是完全顯示全新的資料。這就是Diffable Data Sources

Diffable Data Sources特性-

  • Declarative approach :直接表明最後想要的樣子,而不必告訴data source實際上要如何做。
  • Provide data snapshots:Data Source Object可自行判斷兩個snapshot之間的差異,並自己想辦法apply those changes to collection view.
  • Value must have unique identifiers:Data Source Object就是用這個來判斷差別,所以使用Diffable Data Sources的資料都要服從hashable protocol。
  • Data Source Object 是 從classUICollectionViewDiffableDataSource實例化而來的。實例化時,要說明此Data Source Object將會包含何種型別,也要說明how to populate a cell。
  • 接著我們用classNSDiffableDataSourceSnapshot提供data的snapshot。

解釋完原理後,開始來寫dataSource的method吧!

首先要先創立dataSource屬性,我們要讓他是strong reference,所以吧它放在stored property中。

var dataSource :UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>

dataSource property 是由以下的class實例化而成的,它需要兩個參數,分別是SectionIdentifierTypeItemIdentifierType。 它接受任何型別,所以其實也可以直接寫String在裡面,但是因為使用Type比較準確,所以我們要用enum來定義我們的section type。

class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>

這個type只會在這個viewController中使用,所以我們直接做在ViewController中成為nested type。

目前做的collection view只會有一個section,所以我們只做一個case取名叫main。enum會自動被Swift編譯器賦予Hashable,所以我們也不用再特別寫要服從protocol Hashable

而Item我們現在只想要放數字在裡面,所以直接寫Int,Int本來就服從Hashable,所以我們不用做特別的設定。

確定好section與item的型別後,就可以來寫dataSource啦。

因為之後會把它實例化,所以宣告變數屬性為它。

var dataSource :UICollectionViewDiffableDataSource<Section, Int>!

接下來要實際寫dataSource的method。

首先要實例化UICollectionViewDiffableDataSource<Section, Int>並存進上面宣告的變數dataSource中。

實例化UICollectionViewDiffableDataSource時,要提供兩個參數的值。

第一個需提供UICollectionView型別的物件,這樣這個dataSource object才知道要作用在哪個collectionView上。

第二個要提供一個closure,此closure的目的是闡明how the collection view match the data to each cell。需要三個參數,分別是前面設置好的collectionView,indexPath(才能找到對應的cell),以及Int(這是因為我們先前闡明Item的型別是Int)也就是會顯示在cell上的data。這個colsure會執行在every instance of data from the data source。我們用這個closure告訴dataSource要怎麼把資料顯示在cell上。在此closure主要做兩件事:

  1. 闡明要用的cell,並抓到它的實體
  2. 對已經抓到的cell做設定。
設置dataSource

Step3.設置Snapshot讓以上設定好的CollectionView顯示出來。

在此method中主要做的事:

  1. 實例化NSDiffableDataSourceSnapshot<Section, Int>
  2. 將section, item 加入 snapshot
  3. apply 以上snapshot到dataSource

Step4.在viewDidLoad中執行Step2–4的方法。

方法都寫好了,但要記得執行啊!

Build and Run!

我們現在可以做一點改變,如果想要呈現出以下的畫面,要怎麼進行呢?

我們可以看到,

  • 一排有10個item,item是正方形的。
  • 數字標籤置中,改變cell background color與label的text color。
  • 每個item有格線。

我們一一設定吧!

一排有10個item,item是正方形的。

這可以用item跟group(item的parent container)之間的長寬比來解決。

當item的寬是group的0.1時,代表group會有10個item。

接下來的目標是item的長寬要一樣,才能形成正方形。當然可直接讓item的heightDimension為 .fractionalWidth(0.1)。但為了要更凸顯item, group, section之間的關係,我們要調整group的heightDimension。

切記:.fractional是與parent container的比例。所以對item來說,.fractional是與group的比例 ; 對group來說,.fractional是與section的比例。

而Section的的預設寬會跟整個螢幕一樣寬,他的高取決於他的內容(也就是他裝著的group)。

回到上圖,item的寬是.fractional(0.1),代表item寬是group寬的0.1。

當group的寬是.fractional(1.0),代表item的寬跟section一樣長的,而section寬宥預設等於螢幕寬。所以以上group寬 等於 section寬 等於 螢幕寬。

也就是說item的寬是.fractional(0.1),代表item寬是group寬的0.1,也是section寬的0.1 也是 螢幕寬的0.1。

如果要在group設置高度等於item的widthDimension:.fractionalWidth(0.1)的話,heightDimension: .fractionalWidth(0.1)即可,意即group寬是section寬的0.1。

數字標籤置中,改變cell background color與label的text color。

每個item有格線。

flow layout: define insets using UIEdgeInset

compositional layout:define insets using NSDirectionalEdgeInsets

NSCollectionLayoutItem中,有一個method contentInsets,它可以設定邊框。邊框設置型別為NSDirectionalEdgeInsets。邊框可以是正值或負值,正值表示邊框是長在item外,負值表示邊框是長在item內。

這樣就成功做出來啦!

--

--