※ この記事は2020/7/9時点の内容を元に作成しています。
※ 2020/7/9 ListsのSwipeをハンドリングする設定がUICollectionViewListCell
からUICollectionLayoutListConfiguration
に移動しました。
※ 2020/8/23 textLayoutGuide
について追記しました。
今回もたくさんアップデートがありました
WWDC2019に引き続き
WWDC2020でもUICollectionView
には
様々な新しいAPIが追加されました。
- Sectionベースでの
UICollectionView
の構築 -
UITableView
のようなListsの登場 - Reusableで豊富なdefault設定とカスタマイズが可能なConfigurationの登場
などがあります。
今回はその内容についてまとめてみます。
参照にしたセッションは下記になります。
Advances in UICollectionView
https://developer.apple.com/videos/play/wwdc2020/10097
Advances in diffable data sources
https://developer.apple.com/videos/play/wwdc2020/10045
Lists in UICollectionView
https://developer.apple.com/videos/play/wwdc2020/10026
Modern cell configuration
https://developer.apple.com/videos/play/wwdc2020/10027
Build for iPad
https://developer.apple.com/videos/play/wwdc2020/10105
アップデートの概要
まず今回の更新内容の概要を見ていきます。
UICollectionView
の構成要素
UICollectionView
は
- Data
- Layout
- Presentation
の3つから構成されます。
iOS13以前は
- Data ->
UICollectionViewDataSource
- Layout ->
UICollectionViewLayout
(UICollectionViewFlowLayout
) - Presentation ->
UICollectionViewCell
andUICollectionReusableView
が使用されていました。
そして
UICollectionViewDiffableDataSource
UICollectionViewCompositionalLayout
が登場したことで
この構成に変化がありました。
- Data -> Diffable Data Source
- Layout -> Compositional Layout
- Presentation -> UICollectionViewCell and UICollectionReusableView
※
UICollectionViewDiffableDataSource
と
UICollectionViewCompositionalLayout
に関して
今回詳細は割愛させていただきます。
もし気になる方は以前書いた記事などを参考にしていただけますと幸いです。
https://qiita.com/shiz/items/a6032543a237bf2e1d19
そして今回の更新では
これらの要素の上に新しいAPIの機能が追加されました。
Diffable Data Source
まずDiffable Data Sourceに関してです。
Section Snapshot
既存のNSDiffableDataSourceSnapshot
は
UICollectionView
全体のUIの状態を表現するものでしたが
こちらはSectionごとのUIの状態を表現します。
NSDiffableDataSourceSnapshot
に似ていますが
いくつか異なる点があります。
append
appendはNSDiffableDataSourceSnapshot
にもありますが
注目なのは第二引数です。
mutating func append(_ items: [ItemIdentifierType],
to parent: ItemIdentifierType? = nil)
これはラベルからもわかるように
表示するData間で親子関係を構築することができます。
これによってSection内で階層を表現して
階層構造を持ったリストのようなViewを簡単に構築することができるようになりました。
※
これは今年のアップデートで
Outlineベースと呼ばれるUIを
様々なアプリで採用しているという背景があり
これを実現するためにも必要な機能であったようです。
expand collapse isExpanded
上記のような階層構造をもったSection内で
子要素の折り畳み、展開の管理を
フレームワーク側で管理できるようになりました。
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot/3600721-expand
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot/3600717-collapse
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot/3600729-isexpanded
このメソッドを利用することで
フレームワーク側で表示やSnapshotの状態を自動で更新してくれます。
またユーザの操作でViewが変更された時のハンドリングを行うための
新しいstructも登場しました。(内容は詳細で記載します。)
Reordering Support
ユーザがセルの並び替えを行った際のハンドリングが
非常に簡単にできるようになりました。
主にこのstructを利用します。
https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/3600966-sectionsnapshothandlers
さらに
詳細は後ほど記載しますが
SwiftのCollectionDifference
のAPIを併せて利用することで
自身が管理しているデータとUIの状態の同期が
非常に簡単にできます。
※
CollectionDifference
に関して
今回詳細は割愛させていただきます。
もし気になる方は以前書いた記事などを参考にしていただけますと幸いです。
https://qiita.com/shiz/items/0e363219a0151d790d03
Lists(Compositional Layout)
次にCompositional Layoutです。
今回の更新はListsの登場が中心になります。
Listsの特徴
-
UITableView
のような外見 - スワイプアクションなど
UITableView
で用意されていた機能を利用できる - 多くの共通レイアウトが用意されている
- Compositional Layoutの上で構成されている
- Self-Sizingのサポート
少ないコードの記載で簡単なレイアウトが実現できる
具体的には下記のようなコードを描きます。
※ 様々な詳細は後ほど記載します。
この2行を記載することで
様々なデフォルトの設定が行われ
下記のようなレイアウトが構築できます。
UICollectionLayoutListConfiguration
は
今回新しく登場したstructで
Compositional Layoutと一緒に
UITableViewのようなレイアウトを作成する時に
非常に役立ちます。
appearance
にレイアウトの種類を設定するだけで
UITableView
が持つ標準レイアウトを作成できます。
https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration
https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout/3600951-list
この他にも様々なクラスやメソッドが追加されたことで
デフォルトの設定をそのまま利用できる上に
カスタマイズも比較的自由にできるような設計になっています。
Cell Registration
カスタムなセルを利用する場合に
下記のようなコードを見たことがあるかと思います。
guard let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell", for: indexPath) as? Cell else {
fatalError()
}
この場合
- 文字列でセル名を指定しなければならない
- キャストをする必要がある
- 失敗した場合の処理が必要になる
などのちょっと使いづらい点がありました。
これを解消するために
ExtensionやProtocolを実装していた方もいると思います。
今回新しくCell Registration
というstructが登場し
下記のような形で書けるようになりました。
これによって文字列によるセル名の宣言やキャストをする必要がなくなり
より型安全に実装ができるようになりました。
https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration
https://developer.apple.com/documentation/uikit/uicollectionview/3600945-dequeueconfiguredreusablecell
さらにクロージャ内でセルの設定も行えるため
セルの登録と設定が同じ場所で可能になります。
Content Configuration
新しくUIListContentConfiguration
というstructが追加されたことで
これまでとレイアウトの方法が大きく変わります。
上記のようにUIListContentConfiguration
から
デフォルトのレイアウトを持ったインスタンスを取得し
このインスタンスに対して値を設定していきます。
そして最後にセルにこの設定を適用しています。
一見するとこれまでと変わらないかもしれませんが
実は大きな違いがあります。
これまではUITableViewCell
やUICollectionViewCell
に
直接設定していたことで
同じような設定をする必要があったとしても
個々に値を設定しなければなりませんでした。
しかし
UIListContentConfiguration
が導入されることによって
UITableViewCell
やUICollectionViewCell
さらに他のUIView
に対しても共通の設定を適用することができるようになり
再利用可能なレイアウトが実装しやすくなりました。
UIListContentConfiguration
は
様々なデフォルトの設定を用意しており
呼び出す種類を変更することで
多様なレイアウトを実現できます。
Background Configuration
Content Configurationの他に
Background Configurationがあります。
これはセルのbackgroundに関連した設定を行う時に使用します。
各構成要素のアップデート
ここまで今年のアップデートの概要を紹介してきました。
ここからは各構成要素のアップデートを見ていきます。
Diffable Data Source
Section Snapshot
まずSection Snapshotです。
なぜ導入されたのか
2つの理由が挙げられてました。
- セクション単位のサイズでComposableに構築できるようにしたい
- OutlineスタイルのUIを実現するために階層構造を構築したい
Outlineスタイルとは
下記のようなレイアウトを指します。
Section Snapshotは
Hasable
に適合したTypeであれば
どんなTypeも使用することができます。
また概要の方でも言及しましたが
append
で親子関係を構築することができるようになっています。
UICollectionViewDiffableDataSource
には
新しいapply
メソッドが追加され
Section Snapshotを引数で受け取れるようになりました。
snapshot(for section: Section)
メソッドで
Section SnapShotを取得することもできます。
apply(_:to:animatingDifferences:completion:)
https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/3600964-apply
snapshot(for section: Section)
https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/3600967-snapshot
適用方法
次にどのようにSection Snapshotを適用するかを見ていきます。
大事なポイントとして
Sectionの配列の順番が表示される順番になります。
そして
各Section SnapshotにItemを追加し
UICollectionViewDiffableDataSource
のapply
メソッドから
CollectionViewに適用していきます。
Expansion State
次に折り畳み、展開の機能を見ていきます。
Section Snapshotは
現在自身が折りたたまれているのかどうかの状態を保持しています。
これらのAPIをプログラム内で利用することで
Sectionを折り畳んだり展開したりすることが可能です。
isExpanded
で現在の状態を取得することもできます。
つまり自身でexpansionの状態を管理する必要がありません。
一点注意が必要なのは
Snapshotの状態を変更した時点では画面の表示は変わらず
dataSourceに適用して初めて表示が変更されます。
ユーザのアクションで表示が変更された場合は?
SectionSnapshotHandlers
を使用します。
OutlineのUI(Section Snapshot内で階層構造構築しているUI)で
新しく追加されたOutline Disclosure Accessoryを利用すると(後ほど紹介します)
フレームワーク側が内部の状態を更新してdataSourceへ適用してくれます。
そしてその際に
willExpandItem
willCollapseItem
といったメソッドが呼ばれます。
さらに
snapshotForExpandingParent
を活用することで
必要な時に必要な分だけデータをロードすることができます。
これはappendする際に指定したparentがexpandされた際に呼び出されます。
そのタイミングでデータをロードし
更新したSnapshotを返却することができます。
// Allow every item to be collapsed
dataSource.sectionSnapshotHandlers.shouldCollapseItem = { item in return true }
dataSource.sectionSnapshotHandlers.snapshotForExpandingParent = {
parent, existingSnapshot -> NSDiffableDataSourceSectionSnapshot<String> in
// Return child snapshot for the parent, or just existingSnapshot
}
※
このドキュメント記載を見る限り
expansionを有効にするためは明示的にtrueに設定する必要があるようです。
Reordering
さらにReorderingも
フレームワーク側で管理をしてくれるようになりました。
※
こちらはUICollectionViewDiffableDataSource
へのExtensionの追加ですが
メインはSectionへの実装になりますのSection Snapshotの中で記載させていただきます。
Diffable Data SourceはItemをIdentifierで管理しており
これを活用することでユーザの操作でそのアイテムが移動されたがわかります。
そのためユーザの操作に併せて
内部の状態の更新も行ってくれます。
一方で自身が管理しているデータ(いわゆるBacking Store)の更新(並べ替え)も
必要になります。
そこで今回新しく追加された
ReorderingHandler
を活用します。
この中のdidReorder
で並び替えの結果を取得することができます。
その結果はNSDiffableDataSourceTransaction
に入っています。
initialSnapshot
には移動前の状態が
finalSnapshot
には移動後の状態が入っており
difference
には移動前後で変化のあったItemの一覧が入っています。
NSDiffableDataSourceTransaction
にはCollectionView全体の状態が含まれており
個々のSectionに関してはNSDiffableDataSourceSectionTransaction
に含まれております。
ドキュメントによると下記のようにハンドリングするようです。
// Allow every item to be reordered
dataSource.reorderingHandlers.canReorderItem = { item in return true }
// Option 1: Update the backing store from a CollectionDifference
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
guard let self = self else { return }
if let updatedBackingStore = self.backingStore.applying(transaction.difference) {
self.backingStore = updatedBackingStore
}
}
// Option 2: Update the backing store from the final item identifiers
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
guard let self = self else { return }
self.backingStore = transaction.finalSnapshot.itemIdentifiers
}
Option1は自身が管理しているデータが
CollectionDifference
のAPIを扱える場合
CollectionDifference
を適用するだけで同期が完了します。
Option2では
またデータ自身をidentifierとして使用している場合は
finalSnapshot
のidentifier
をそのまま適用できます。
Lists(Compositional Layout)
次にListsです。
再喝になりますが
Listsには
-
UITableView
のような外見 - スワイプアクションなど
UITableView
で用意されていた機能を利用できる - 多くの共通レイアウトが用意されている
- Compositional Layoutの上で構成されている
- Self-Sizing
などの特徴があります。
Self-Sizingがデフォルト
中でもSelf-Sizingがデフォルトの挙動となっているため
自身でセルの高さを計算する必要はないと
Appleは言っております。
一方で独自で計算したい場合は
preferredLayoutAttributesFittingAttributes
をoverrideすることで
実現することができるようです。
https://developer.apple.com/documentation/uikit/uicollectionreusableview/1620132-preferredlayoutattributesfitting
UICollectionLayoutListConfiguration
Listsの中で重要な役割を担っているのが
UICollectionLayoutListConfiguration
です。
UICollectionLayoutListConfiguration
は
NSCollectionLayoutSection
の上に構築されています。
例えば下記のように使用した場合
let configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
各Sectionに同じレイアウトを適用することができます。
一方で
let layout = UICollectionViewCompositionalLayout() { sectionIndex, layoutEnvironment in
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.headerMode = sectionIndex == 0 ? .supplementary : .none
let section = NSCollectionLayoutSection.list(using: configuration,
layoutEnvironment: layoutEnvironment)
return section
}
とすることで
個々のSectionにそれぞれのレイアウトを適用することも可能です。
ここから
より詳細を見ていきます。
Appearance
ListsではこれまでUITableView
で実現していたレイアウトと
同じようなレイアウトを実現するためのオプションを用意しています。
.plain
.grouped
.insetGroup
さらにiPadOS14で登場したマルチコラムのレイアウト実現するために
新しく下記の2つのオプションも用意しています。
.sidebar
.sidebarPlain
Separators Headers Footers
さらに
Listsでは
Separators
Headers
Footers
の表示方法などを
詳細にコントロールすることができます。
Headers Footers
UITableView
と異なる点として
HeaderやFooterは明示的にOnにしないと表示されません。
設定方法は2つがあります。
-
supplementaryView
として使用する - (Headerのみ)
firstItemInSection
として使用する
https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration/headermode
https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration/footermode
supplementaryView
下記の例では
HeaderをsupplementaryView
として使用しています。
この場合
画面にレンダリングされる際にViewを提供する
実装をする必要があります。
一番簡単な方法としては
Diffable Data Sourceの
supplementaryViewProvider
を実装することです。
ただし
UICollectionViewDelegate
を使っても
実装することができます。
(注意)各セクションにHeaderの設定が必要
1点気をつけなければならない点として
Headerを使用する場合
使用しないSectionにも設定が必要になります。
これを行わずnilをreturnすると
assertionエラーが起きます。
そのため下記のように.none
を明示的に設定する必要があります。
firstItemInSection
次にfirstItemInSection
です。
これはSectionの最初のセルをHeaderのように扱うように設定します。
この設定は
今回新しく登場した
階層構造を構築するようなレイアウトに使うことが
推奨されています。
ドキュメントにも下記のように書かれています。
Choose this header mode when you’re using hierarchical data sources
to be able to expand and collapse the header.
この場合はセルの構築を行う中で
明示的に設定する必要があります。
dataSource = UICollectionViewDiffableDataSource<Int, Item>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? in
// ↓↓↓↓↓↓条件分岐が必要になる
if indexPath.item == 0 {
return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: item)
} else {
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
}
List Cell
次にList Cellです。
iOS14では
UICollectionViewListCell
というUICollectionViewListCell
のサブクラスが
新しく追加されました。
これはListスタイルのレイアウトを実現するために
下記の項目に関して
多くのデフォルト設定が用意されており
さらに独自にカスタマイズできるようになっています。
個々の項目について見ていきます。
Separators
まずSeparatorsです。
これはよく見かけるようなレイアウトだと思いますが
例えばSeparatorをラベルの位置を合わせたいとします。
(ラベルとSeparatorのleadingを合わせたいとします。)
UITableView
の場合
Separator insetを調整することができます。
しかし
- Safe area insets
- Dynamic font size
- SFSymbols
といったことを考慮し始めると
全ての場合において最適な状態にすることは難しくなります。
そこで
UICollectionViewListCell
には
新しくSeparator Layout Guideが登場しました。
このLayout Guideは
これまでのLayout Guideの設定方法とは異なり
Layout Guide自身に制約を設定して
Contentsに位置を合わせます。
ドキュメントによりますと
By default, when you apply a system-provided content configuration to a list cell,
the separator automatically aligns to the primary text in the content view.
For custom subviews in the cell,
you need to add a constraint to this layout guide
that connects it to the leading edge of the cell’s primary content.
フレームワーク側が提供している設定を利用している場合
デフォルトでprimary textにSeparatorに合わせるようになっているようです。
※
このprimaryの基準がどういうものなのか追えていないので
もしご存知の方がいらっしゃいましたら教えてください🙇♂️
Swipe Action
次にSwipe Actionです。
UITableView
とは異なり
Swipe ActionはList Cellの一つの機能になっています。
そのため
セルの構築を行う際に一緒に設定をすることができます。
ただし
この機能はレイアウトとセルの間でのやりとりが必要になるため
list configurationを使用したセクションの中でのみ動きます。
独自のアクションを実装したい場合は
- leadingSwipeActionsConfiguration
- trailingSwipeActionsConfiguration
をOverrideすることで実現できます。
https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3601205-leadingswipeactionsconfiguration
https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3601207-trailingswipeactionsconfiguration
注意点
気をつけなければならない点としては
IndexPath
を使用しないということです。
IndexPath
はセルが挿入や削除されると位置がずれます。
仮にIndexPath
でデータを管理している場合などは
間違ったデータの更新をしてしまう可能性があります。
そこでIdentifier
を使用することで
そのリスクをなくすことができます。
※
本題とは関係ありませんが
UITableViewRowAction
はiOS14でdeprecatedになりました。
https://developer.apple.com/documentation/uikit/uitableviewrowaction
追記(2020/7/9)
Xcode12.0 Beta2で
UICollectionLayoutListConfiguration
の
leadingSwipeActionsConfigurationProvider
trailingSwipeActionsConfigurationProvider
に移動したようです。
サンプルでは
下記のように設定しています。
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.leadingSwipeActionsConfigurationProvider = { [weak self] (indexPath) in
guard let self = self else { return nil }
guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil }
return self.leadingSwipeActionConfigurationForListCellItem(item)
}
Accessories
次にAccessoriesです。
UITableView
では
- 扱える種類が少ない
- trailingにしか使えない
などの制約がありましたが
List Cellでは
かなり豊富な種類のAccessoriesを扱え
leading、trailing両方に設定できるようになりました。
さらにどちら側にも複数のAccessoriesを設定することが可能です。
また
UITableViewでは
Accessoriesは単なる装飾的な存在でしたが
List CellではAccessoriesを追加することで
Cellに機能を追加することができます。
例えば
このReorderを設定することで
ユーザがセルをタップをすると自動でCollectionViewがReorderモードになります。
DeleteをタップするとtrailingにSwipe Actionが表示されたり
Outline Disclosureはセクションのexpand collapseができるようになります。
※
ただし現在のXcode12.0 Betaですと
サンプルコードでは
上記3つの中で
disclosureIndicator
以外は
変更しても表示されませんでした。
設定方法はセルのaccessoriesというプロパティに設定します。
表示方法ですが
デフォルトでは
UIKit側でどこに何を配置するのが最適かを
種類から判断し
自動で設定していると
セッションでは言っていました。
※
その基準がどうなっているのかまでは追えていないので
ご存知の方がいらっしゃいましたら教えてください🙇♂️
一方で様々に調整できるOptionも用意されいます。
例えば
下記ではEditモードではない場合にのみ
表示することができます。
位置に関しては下記のようなenumがあるので変更できるようです。
https://developer.apple.com/documentation/uikit/uicellaccessory/placement
今回紹介した以外にも様々なAccessoriesが用意されています。
例えばメールアプリではチェックマークが使用されています。
View Configuration
次にViewのConfigurationです。
これまではセルに直接値の設定をしていました。
これがConfigurationへ設定するように変わり
最後にcellのcontentConfiguration
へ適用します。
これは一見すると同じように見えますが
contentConfiguration
に設定することで
特定の種類のセルだけではなく
contentConfiguration
に対応している全てのViewに
同じ設定を適用できるようになります。
※
ドキュメントで検索するとCellとHeaderFooterViewで利用できるようです。
UIListConfiguration
はデフォルトのレイアウトが用意されているため
少ない設定でCollectionViewに標準的なレイアウトを適用することができます。
2つのConfiguration
これまではList Content Configurationを見てきましたが
Configurationには2つの種類があります。
UIBackgroundConfiguration
UIListContentConfiguration
Background Configuration
Background Configurationには
セルのバックグラウンドに関連するプロパティの設定をします。
下記のように
backgroundConfiguration
に設定します。
List Content Configuration
そして
List Content Configurationは
UIContentConfiguration
に適合したUIListContentConfiguration
は
標準的なUITableView
のセルやHeader、Footerのレイアウトを提供します。
UIContentConfiguration
https://developer.apple.com/documentation/uikit/uicontentconfiguration
また
個々のプロパティも設定できるため
カスタマイズも比較的自由にできます。
上記でも見ましたが
下記が設定方法です。
推奨される使用方法
これらのConfigurationは
軽量でインスタンス生成のコストが非常に小さい
とセッション内では言っています。
また
Configurationはstructであり
cellに適用するまでは他に影響を与える心配がありません。
そこで推奨する使用方法としては
毎回defaultConfiguration
メソッドから
新しいインスタンスを生成し
新しいconfigurationを設定していくのが良いと
セッション内では言っています。
こうすることで
古い状態に気にする必要もなくなりますし
複雑な状態や遷移処理が必要な場合にも
新しいconfigurationを毎回設定するだけになります。
さらに
Configurationはパフォーマンスにも良いと言っています。
これはUIKitと内部で連携することで
パフォーマンスの最適化が実現できていることによるようです。
Configuration State
次にConfiguration Stateです。
これは
セルやViewのレイアウトを設定する際に
現在セルがどのような状態にあるのかをプロパティとして保持しています。
これは先ほど見た
Configurationの出力結果です。
Configurationを利用することで
このような状態が異なる外見を
自動で利用できるようになります。
Configurationの実態は
特定の状態に応じた外見の記述です。
では
どういう状態があるのかを見ていきます。
Traits
まず一番大きい単位として
下記のようなUITraitCollection
の値があります。
States
次に
CellやViewがUIKitが内部に保持する状態があります。
Custom States
さらにその上に自身のカスタムな状態も実装することができます。
UIConfigurationState
これらの状態は
UIConfigurationState
を利用します。
https://developer.apple.com/documentation/uikit/uiconfigurationstate
具体的には
UIViewConfigurationState
UICellConfigurationState
を用いて保持します。
UICollectionView
とUITableView
のどちらにも適用できます。
UIViewConfigurationState
Trait Collectionの下に
下記のStateが存在します。
- Highlighted
- Selected
- Disabled
- Focused
さらにそこに自身のカスタムなStateを追加できます。
UIConfigurationStateCustomKey
を使います。
ドキュメントに使用例があります。
// Declare a custom key for a custom isArchived state.
extension UIConfigurationStateCustomKey {
static let isArchived = UIConfigurationStateCustomKey("com.my-app.MyCell.isArchived")
}
// Declare an extension on the cell state structure to provide a typed property for this custom state.
extension UICellConfigurationState {
var isArchived: Bool {
get { return self[.isArchived] as? Bool ?? false }
set { self[.isArchived] = newValue }
}
}
class MyCell: UICollectionViewCell {
// This is an existing custom property of the cell.
var isArchived: Bool {
didSet {
// Ensure that an update is performed whenever this property changes.
if oldValue != isArchived {
setNeedsUpdateConfiguration()
}
}
}
override var configurationState: UICellConfigurationState {
// Get the structure from UIKit with the system properties set by calling super.
var state = super.configurationState
// Set the custom property on the state.
state.isArchived = self.isArchived
return state
}
override func updateConfiguration(using state: UICellConfigurationState) {
// Get the default background configuration styling for the current state based on the system states.
var backgroundConfig = UIBackgroundConfiguration.listPlainCell().updated(for:
state)
// Customize the background configuration based on the custom state.
if state.isArchived {
backgroundConfig.visualEffect = UIBlurEffect(style: .systemMaterial)
}
// Apply the background configuration to the cell.
self.backgroundConfiguration = backgroundConfig
}
}
UICellConfigurationState
UICellConfigurationState
は
UIViewConfigurationState
にさらに
- Editing
- Swiped
- Expanded
- Drag and Drop
を追加した状態保持しています。
カスタムな状態の実装方法は同じです。
Configurationの更新
updateConfiguration(using:)
を用います。
https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration
まず現在の状態に応じたConfigurationを取得し
そのconfigurationに値を設定していきます。
このメソッドは直接呼ぶのではなく
setNeedsUpdateConfiguration
を呼び出すことで間接的に呼び出すようにします。
デフォルトでは
Configuration Stateが更新される度に
このメソッドが呼ばれ、
新しい状態に応じたConfigurationを自動で返します。
この動作を抑えるためには
automaticallyUpdatesContentConfiguration
automaticallyUpdatesBackgroundConfiguration
をfalseにします。
使用例
これまで紹介したものと同じように
最初のConfigurationを取得し
状態に応じて設定の変更を行い
最後にconfigurationをセルに設定します。
UIConfigurationColorTransformer
上記の例でも
ステータスに応じてカラーを設定していましたが
UIConfigurationColorTransformer
を使ってよりカスタマイズしやすい形で設定することができます。
UIConfigurationColorTransformer
はColorを引数に受け取り
別のColorに変換する一つのメソッドを持つstructです。
これを用いることで
同じColorから様々な状態に応じたColorを作り出すことができます。
Dynamic Layoutにも自動で対応
Dynamic Font Typeや
デバイスに応じたレイアウトにも自動で対応しています。
layout Margins
Content Configurationは
MarginやPaddingを調整することができます。
この値と実際に表示される値から
intrinsic heightが計算され
Self-Sizingされます。
Image reserved layout size
最後にデフォルトの設定から
調整が必要な例が挙げられていましたので紹介します。
下記のレイアウトは
それぞれ異なるイメージですが
幅が一緒のイメージが使用されているおり
レイアウトに問題はなさそうです。
しかし
異なる幅の画像を使用すると
デフォルトの設定では
テキストは左揃えになりません。
これはイメージがleadingに揃える設定になっていることと
イメージとテキストの間のPaddingが同じ値になるように設定されていることが原因です。
これをテキストが左揃えになるように調整するために
reservedLayoutSize
を使用します。
すべてのセルに同じ幅を設定することによって
イメージがreservedLayoutSize
に合わせて
水平に真ん中寄せになります。
イメージの大きさには影響はありません。
(2020/8/23追記)textLayoutGuide
UIListContentConfiguration
にtext
(= primary text)を設定している場合に
そのtext
の位置を基準にしたlayoutGuideを取得できます。
Appleのサンプルでは
separatorLayoutGuide
と合わせて使って
separatorをtextに合わせるようにしています。
private var separatorConstraint: NSLayoutConstraint?
private func updateSeparatorConstraint() {
guard let textLayoutGuide = listContentView.textLayoutGuide else { return }
if let existingConstraint = separatorConstraint, existingConstraint.isActive {
return
}
let constraint = separatorLayoutGuide.leadingAnchor.constraint(equalTo: textLayoutGuide.leadingAnchor)
constraint.isActive = true
separatorConstraint = constraint
}
secondaryText
には
secondaryTextLayoutGuide
があります。
https://developer.apple.com/documentation/uikit/uilistcontentview/3650225-secondarytextlayoutguide
image
には
imageLayoutGuide
があります。
https://developer.apple.com/documentation/uikit/uilistcontentview/3650224-imagelayoutguide
既存の実装にConfigurationを追加時の注意点
いくつかConfigurationを使用する上での注意点が述べられていました。
Background configurationは項目をリセットする
Background configurationを使用すると
background colorやbackground viewはnilになるようです。
そのため既存の設定が反映されなくなる可能性があります。
Content configurationは値を上書きする。
Content configurationが保持しているプロパティで
- imageView
- textLabel
- detailedTextLabel
のようなセル、Header、Footerの
ビルトインのsub viewが保持している
プロパティは上書きされます。
※
内容と直接関係ありませんが
こういったプロパティを直接Viewに設定する既存の方法は
将来的にdeprecatedになるようです。
## 他のViewと一緒にListsを利用する
UIListContentView
を利用することで
他のViewと一緒に使うことができます。
https://developer.apple.com/documentation/uikit/uilistcontentview
使い方としては
これまでと同様に
UIListContentConfiguration
に値を設定して
UIListContentView
に反映し
これをSubViewとして追加すれば可能です。
UIListContentConfiguration
が保持している
デフォルトのスタイルを利用しCustom Viewを簡単に作れるようになります。
UIListContentView
はUIView
なので
CollectionView
やTableView
以外でも
利用可能です。
Custom Configurationを作成する
自身でConfigurationを作成することもできます。
その際は
UIContentConfiguration
に適合させたstructを定義します。
https://developer.apple.com/documentation/uikit/uicontentconfiguration
そしてViewにはUIContentView
プロトコルに適合したViewを作成し
configuration
プロパティに設定します。
https://developer.apple.com/documentation/uikit/uicontentview
https://developer.apple.com/documentation/uikit/uicontentview/3600985-configuration
Configurationを利用することで
Viewが新しい状態に変わった時の自動アップデートなど
多くのメリットを活用することができます。
全体的なUICollectionViewの構築例
最後になりますが
セッション内で紹介されていた
UICollectionView
の構築方法を見ていきます。
下記のようなレイアウトをリストを構築します。
レイアウト
Configurationの設定
まず
UICollectionLayoutListConfiguration
を用いて
具体的なレイアウトの内容を設定します。
今回は.sidebar
のスタイルを利用しています。
UICollectionViewCompositionalLayout
に設定
そして
UICollectionViewCompositionalLayout
のlist
メソッドに
引数として先ほどのUICollectionLayoutListConfiguration
を渡すことで
垂直にスクロールするレイアウトが作成できます。
UICollectionView
に適用
最後にUICollectionView
に
UICollectionViewCompositionalLayout
を設定します。
データの設定
次にデータの設定です。
セルの内容と自身のモデルを繋げます。
この例では
title
-
image
を保持したMyItem
と呼ばれるモデルを利用しています。
セルの登録
まずは利用するセルとモデルを
CellRegistration
を利用して登録します。
クロージャ内で
具体的にモデルをセルをどうやって設定していくのか
を定義します。
ここでは
defaultContentConfiguration
メソッドで取得したインスタンスを
そのまま利用しています。
title
とimage
をcontentに設定し
最後にセルに設定を反映させます。
Data Sourceの生成
データとセルは
Data Sourceを経由して繋がります。
表示が必要になった際に
Data Sourceのクロージャで定義された方法で
セルを構築していきます。
今回はUICollectionViewDiffableDataSource
を利用しています。
Section TypeとItem Typeを設定して
Diffable Data Sourceを生成します。
Diffable Data Sourceは
具体的なCollectionViewを引数に受け取り初期化します。
セルを構築する際には
クロージャで
collectionView
, indexPath
, item
を受け取って
構築していきます。
クロージャではCellを返します。
dequeueConfiguredReusableCell
で
先ほど作成したCellRegistration
を渡します。
これだけでセルの再利用などは
フレームワークが自動で管理してくれるようになります。
結果
最終的に下記のようなリストが構築できます。
セルの状態に応じてbackground colorなども
自動で変更されるようになっています。
appearanceに.sidebarPlain
を設定すると
下記のようなレイアウトになります。
サンプル
こちらのサンプルが非常にわかりやすくシンプルに実装されています。
https://github.com/kharrison/CodeExamples/tree/main/ListCollection
まとめ
今年のアップデートを見ていきました。
昨年に引き続き
たくさんのアップデートがありました。
特に今年はUITableViewにとって変わるような
Listsが登場したことが大きく印象に残っています。
デフォルトで必要な設定は何もせずに利用できることで
コードを短くすることができる上に
カスタマイズができる余地も与えている点が
非常にバランスが取れているのではないかと感じています。
そしてUITableViewは
今後なくなっていくのではないかと思いました。
(セッションの中でも置き換えることを推奨しているような発言がありました)
もう一つ気になった点としては
Section Snapshotで
SectionSnapshotHandlers
ReorderingHandlers
といったstructが用いられていた点です。
これまでUIKitではdelegateパターンを用いて
ユーザのアクションなどを受け取る設計が多く見られましたが
今回のようなstructを利用することで
定義とハンドリング処理を同じ場所で
より宣言的にハンドリング処理を記載できるなど
さらに見やすくなったのではないかなと個人的には思いました。
より具体的にどういう風に利用するのかを
見たい方は公式のサンプルコードがおすすめです。
これを見れば
今年の更新内容や
昨年登場したDiffableDataSource
、CompositionalLayout
の使い方も把握できると思います。
UICollectionView
は
どんどん進化していっているので
今後もアップデートが楽しみです😀
間違いなどございましたらご指摘頂けますと嬉しいです🙇♂️