有兩種方式可以製作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。
以下是簡略的重點步驟:
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之間)。
Group:
設置group,就是實例化NSCollectionLayoutGroup
這個class。
Group is a flexible container to contain Item.
實例化NSCollectionLayoutGroup
這個class時,需要選擇是horizontal還是vertical;也需標明此Group的內容(subItem)是什麼 ; 需要提供size,size的型別同樣是NSCollectionLayoutSize
。
Section:
Section的設置比較簡單,他的預設寬會跟整個螢幕一樣寬,他的高取決於他的內容(也就是他裝著的group)。所以只需要標明Section內裝著的group即可。
item, group, section都設置好了之後,就可以return UICollectionViewCompositionalLayout
以下是目前,設置好layout的狀態。接下來要處理顯示資料內容。
*在storyBoard客製化cell。
collectionView沒有預設好的cell,所以都要自己做。
先把cell的長寬拉出來,在這裡拉的話,還是會被在code裡的設定覆蓋掉,但在這裡做出來有助於我們知道自己在做什麼。
接著加入label,然後給這個cell一個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 是 從class
UICollectionViewDiffableDataSource
實例化而來的。實例化時,要說明此Data Source Object將會包含何種型別,也要說明how to populate a cell。 - 接著我們用class
NSDiffableDataSourceSnapshot
提供data的snapshot。
解釋完原理後,開始來寫dataSource的method吧!
首先要先創立dataSource屬性,我們要讓他是strong reference,所以吧它放在stored property中。
var dataSource :UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
dataSource property 是由以下的class實例化而成的,它需要兩個參數,分別是SectionIdentifierType
與ItemIdentifierType。
它接受任何型別,所以其實也可以直接寫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主要做兩件事:
- 闡明要用的cell,並抓到它的實體
- 對已經抓到的cell做設定。
Step3.設置Snapshot讓以上設定好的CollectionView顯示出來。
在此method中主要做的事:
- 實例化
NSDiffableDataSourceSnapshot<Section, Int>
- 將section, item 加入 snapshot
- 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內。
這樣就成功做出來啦!