20200701のiOSに関する記事は4件です。

iOSのリバースエンジニアリングガイドを作った(再)

勉強中の身でありながら公開させていただくのは恐縮なのですが〜❣❣

iOS App REガイド

今の所本筋(RE関連)よりサイドコンテンツから増やしていくつもりです,

サイドコンテンツ(いずれも鋭意執筆中)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存アプリを一部Flutter化する(Add-to-app)

Flutterには既存のNativeアプリの一部分をFlutter化するソリューションであるAdd-to-appが存在するので試してみる。

Androidでは特定のActivityをFlutter化出来たり、特定のFragmentをFlutter化出来たりと結構柔軟に対応できそうである。
iOSに関しては特定のViewControllerをFlutter化可能である。

Flutter Module(Flutter化された画面というか普通にFlutterの部分)を作成して、Android、iOSそれぞれから呼び出すまでの手順を示す。

制限

Flutter v1.12 Add-to-appには下記制限が存在する。

  • 複数のFlutterインスタンスを動作させたり、Viewの一部で動作させると挙動がおかしくなる可能性がある。
  • バックグラウンド動作モードは試作中
  • 複数のFlutterライブラリをアプリに同封するのはサポートされていない
  • add-to-appで使用するFlutterPluginは最新の「new Android plugin API」に対応してないといけない
    • Flutter Activityが常に存在しているなどを想定した動作を実装していると挙動がおかしくなる可能性がある。
  • AndroidXアプリしかサポートしていない

Androidプロジェクトへの組み込み、実行

Androidプロジェクト上でFlutterModuleを作成、実行。その後同ModuleをiOSに組み込むという手順で実施してみる。

Flutter Module作成

公式に記載されている「Using Android Studio」の章通りに実行すれば可能。
FlutterModuleのプロジェクトがAndroidアプリとは別のディレクトリに作成されて、AndroidStudio上で上手に内包される感じになる。
後にこのFlutterModuleのプロジェクトをiOSのプロジェクトにも取り込む。

Flutter Moduleの実行方法

公式通りに実行すれば問題ない。

実行方法を大きく分けるとFlutterActivityを生成して単一画面上で実行する方法と、FlutterFragmentを生成して臨機応変な場面で実行する方法に別れる。
基本的にはシンプルなFlutterActivityでの実行を推奨している。
※iOSはViewControllerでの実行しかサポートしていないので、単一画面上でしか実行出来ないものと思われる(すいませんここはiOSそこまで詳しくないです)。となると同一FlutterModuleをAndroidでもiOSでも流用すると考えるとFlutterActivityでの実行を選択することになると思われる。

  • FlutterActivity
    • 普通にActivityを起動する感じ
    • 初期Rootを指定できる
    • FlutterEngineが起動する時間があるので、予めEngineを起動してスタートする事も可能
  • FlutterFragment
    • ActivityのFragment版
    • 基本はシンプルなActivityを使用することを推奨している
    • いろんなActivityのコールバックをFlutterFragmentに通知する必要がある

Pluginのマネジメント

firebase_crashlyticsプラグインみたいにAndroidアプリのGradleの編集を要求するようなプラグインを使用する場合には別途対応が必要である。

iOSプロジェクトへの組み込み、実行

上記で作成したFlutter ModuleをiOSのプロジェクトに組み込んで起動するまでの手順を示す。

iOSプロジェクト作成

Flutter Moduleは作成済みなので公式の「Embed the Flutter module in your existing application」の章からCocoapodsを利用した組み込みを実施する。

  1. Xcodeで新規のStoryboardプロジェクトを作成
  2. CocoaPodsを適用
$ cd プロジェクトパス
$ pod init
$ pod install
Please close any current Xcode sessions and use `Flutter_test.xcworkspace` for this project from now on.
Pod installation complete! There are 0 dependencies from the Podfile and 0 total pods installed.
  1. Flutterへの依存をPodfileに記載

image.png

flutter_application_pathに設定するのがAndroidStudioから作成したFlutte Moduleへのパスである。

4. 依存を解消

$ pod install

Flutter Moduleの実行方法

公式に書いてあるとおりに実行すればFlutterが走るViewControllerが起動する。
Androidと同様FlutterEngineを予め起動することが可能である。

参考

https://flutter.dev/docs/development/add-to-app
https://qiita.com/imamurh/items/43966e33e100956e0998

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】ジェネリクスについて改めて調べてみた

業務でSwiftに携わり始めた頃、クラスやメソッド名の隣に書かれている<>の意味が分からずに困っていました。しかもなんて調べたら<>←この正体に出会えるのかも分からない。
随分後になってこれがジェネリクスという記法だということを知りました。

そんなジェネリクスについてまとめたいと思います。

ジェネリクスとは

汎用的な実装をするための技術です。
通常関数の引数はIntStringなど、型を決めて宣言します。もちろん宣言した型以外は変数に入れることはできません。しかしジェネリクスを使うと、同じ関数の引数に違う型を渡すことができるのです。

Swiftにはジェネリック関数ジェネリック型があります。

ジェネリック関数

型引数(<>で囲まれたやつ)をもつ関数のことです。

func get<T: Equatable>(_ x: T) -> T {
    // 返り値は引数と同じ型になる
    return x
}

get(1) // 1: Int
sum("a") // a: String

// 複数の引数を違う方で受け取りたい場合、型引数を2つ定義する
func get<T, U>(_ x: T, y: U) { }

get(1, "String")

<T: Equatable>Equatableプロトコルに準拠した型を意味します。これを型制約と言います。
sum(1, 2)のように、ジェネリック関数またはジェネリック型に具体的な型引数を与えて型を確定することを特殊化と言います。

ジェネリック型

型引数をもつクラスや構造体・列挙型のことです。

// クラス
class SampleClass<T> {}
// 構造体
struct SampleStruct<T> {}
// 列挙型
enum SampleEnum<T> {}

型の安全性は保たれるのか?

ジェネリクスは汎用性と型の安全性を兼ね備えています。対象にジェネリクスと同様に汎用的な実装を可能にするAnyは型の安全性を備えていません。Anyは暗黙的に型推論される為、具体的な型として扱いたい時はダウンキャストが必要になります。

func get<T: Equatable>(_ x: T) -> T {
    return x
}

get(1) // 1: Int

// ------- 先ほどのこれをAnyを使って実現させたい場合---------

func get(_ x: Any) -> Any { 
   if let intX = x as? Int { return intX }
}

let anyGet = get(1) // Any型
if let intGet = anyGet as? Int { // get(1)のInt型が取れる }

Anyは具体的な引数を渡してもAny型のままなので、こんな感じで都度ダウンキャストする必要があります。

ジェネリクスとAnyを使い分けて活用すればかなり汎用的な実装が可能になりそうですね。

*2020/7/1 コメント頂きもう少し分かりやすく修正しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】UICollectionView WWDC2020 アップデートまとめ

※ この記事は2020/7/1時点の内容を元に作成しています。

今回もたくさんアップデートがありました

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 and UICollectionReusableView

が使用されていました。

image.png

そして

UICollectionViewDiffableDataSource
UICollectionViewCompositionalLayout

が登場したことで
この構成に変化がありました。

  • Data -> Diffable Data Source
  • Layout -> Compositional Layout
  • Presentation -> UICollectionViewCell and UICollectionReusableView

image.png


UICollectionViewDiffableDataSource
UICollectionViewCompositionalLayoutに関して
今回詳細は割愛させていただきます。

もし気になる方は以前書いた記事などを参考にしていただけますと幸いです。
https://qiita.com/shiz/items/a6032543a237bf2e1d19

そして今回の更新では
これらの要素の上に新しいAPIの機能が追加されました。

image.png

Diffable Data Source

まずDiffable Data Sourceに関してです。

Section Snapshot

image.png

既存のNSDiffableDataSourceSnapshot
UICollectionView全体のUIの状態を表現するものでしたが
こちらはSectionごとのUIの状態を表現します。

https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot

NSDiffableDataSourceSnapshotに似ていますが
いくつか異なる点があります。

append

https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesectionsnapshot/3600716-append

appendはNSDiffableDataSourceSnapshotにもありますが
注目なのは第二引数です。

mutating func append(_ items: [ItemIdentifierType], 
                     to parent: ItemIdentifierType? = nil)

これはラベルからもわかるように
表示するData間で親子関係を構築することができます。

これによってSection内で階層を表現して
階層構造を持ったリストのようなViewを簡単に構築することができるようになりました。


これは今年のアップデートで
Outlineベースと呼ばれるUIを
様々なアプリで採用しているという背景があり
これを実現するためにも必要な機能であったようです。

image.png

expand collapse isExpanded

上記のような階層構造をもったSection内で
子要素の折り畳み、展開の管理を
フレームワーク側で管理できるようになりました。

image.png

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も登場しました。(内容は詳細で記載します。)

image.png

https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/sectionsnapshothandlers

Reordering Support

ユーザがセルの並び替えを行った際のハンドリングが
非常に簡単にできるようになりました。

image.png

主にこの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のサポート

少ないコードの記載で簡単なレイアウトが実現できる

具体的には下記のようなコードを描きます。
※ 様々な詳細は後ほど記載します。

image.png

この2行を記載することで
様々なデフォルトの設定が行われ
下記のようなレイアウトが構築できます。

UICollectionLayoutListConfiguration
今回新しく登場したstructで
Compositional Layoutと一緒に
UITableViewのようなレイアウトを作成する時に
非常に役立ちます。

appearanceにレイアウトの種類を設定するだけで
UITableViewが持つ標準レイアウトを作成できます。

image.png

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が登場し
下記のような形で書けるようになりました。

image.png

これによって文字列によるセル名の宣言やキャストをする必要がなくなり
より型安全に実装ができるようになりました。

https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration
https://developer.apple.com/documentation/uikit/uicollectionview/3600945-dequeueconfiguredreusablecell

さらにクロージャ内でセルの設定も行えるため
セルの登録と設定が同じ場所で可能になります。

Content Configuration

新しくUIListContentConfigurationというstructが追加されたことで
これまでとレイアウトの方法が大きく変わります。

https://developer.apple.com/documentation/uikit/uilistcontentconfiguration

image.png

上記のようにUIListContentConfigurationから
デフォルトのレイアウトを持ったインスタンスを取得し
このインスタンスに対して値を設定していきます。

そして最後にセルにこの設定を適用しています。

一見するとこれまでと変わらないかもしれませんが
実は大きな違いがあります。

これまではUITableViewCellUICollectionViewCell
直接設定していたことで
同じような設定をする必要があったとしても
個々に値を設定しなければなりませんでした。

しかし
UIListContentConfigurationが導入されることによって
UITableViewCellUICollectionViewCell
さらに他のUIViewに対しても共通の設定を適用することができるようになり
再利用可能なレイアウトが実装しやすくなりました。

UIListContentConfiguration
様々なデフォルトの設定を用意しており
呼び出す種類を変更することで
多様なレイアウトを実現できます。

image.png

image.png

Background Configuration

Content Configurationの他に
Background Configurationがあります。

これはセルのbackgroundに関連した設定を行う時に使用します。

image.png

https://developer.apple.com/documentation/uikit/uibackgroundconfiguration

各構成要素のアップデート

ここまで今年のアップデートの概要を紹介してきました。
ここからは各構成要素のアップデートを見ていきます。

Diffable Data Source

Section Snapshot

まずSection Snapshotです。

image.png

なぜ導入されたのか

2つの理由が挙げられてました。

  • セクション単位のサイズでComposableに構築できるようにしたい
  • OutlineスタイルのUIを実現するために階層構造を構築したい

Outlineスタイルとは
下記のようなレイアウトを指します。

image.png

image.png

Section Snapshotは
Hasableに適合したTypeであれば
どんなTypeも使用することができます。

また概要の方でも言及しましたが
appendで親子関係を構築することができるようになっています。

image.png

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を適用するかを見ていきます。

image.png

大事なポイントとして
Sectionの配列の順番が表示される順番になります。

そして
各Section SnapshotにItemを追加し
UICollectionViewDiffableDataSourceapplyメソッドから
CollectionViewに適用していきます。

Expansion State

次に折り畳み、展開の機能を見ていきます。

image.png

Section Snapshotは
現在自身が折りたたまれているのかどうかの状態を保持しています。

これらのAPIをプログラム内で利用することで
Sectionを折り畳んだり展開したりすることが可能です。

isExpandedで現在の状態を取得することもできます。
つまり自身でexpansionの状態を管理する必要がありません。

一点注意が必要なのは
Snapshotの状態を変更した時点では画面の表示は変わらず
dataSourceに適用して初めて表示が変更されます。

ユーザのアクションで表示が変更された場合は?

SectionSnapshotHandlersを使用します。

image.png

https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/sectionsnapshothandlers

OutlineのUI(Section Snapshot内で階層構造構築しているUI)で
新しく追加されたOutline Disclosure Accessoryを利用すると(後ほど紹介します)
フレームワーク側が内部の状態を更新してdataSourceへ適用してくれます。

image.png

そしてその際に
willExpandItem
willCollapseItem
といったメソッドが呼ばれます。

さらに

snapshotForExpandingParentを活用することで
必要な時に必要な分だけデータをロードすることができます。

これはappendする際に指定したparentがexpandされた際に呼び出されます。
そのタイミングでデータをロードし
更新したSnapshotを返却することができます。

image.png

// 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
}

https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/sectionsnapshothandlers/3600961-snapshotforexpandingparent

※ 
このドキュメント記載を見る限り
expansionを有効にするためは明示的にtrueに設定する必要があるようです。

Reordering

さらにReorderingも
フレームワーク側で管理をしてくれるようになりました。


こちらはUICollectionViewDiffableDataSourceへのExtensionの追加ですが
メインはSectionへの実装になりますのSection Snapshotの中で記載させていただきます。

image.png

Diffable Data SourceはItemをIdentifierで管理しており
これを活用することでユーザの操作でそのアイテムが移動されたがわかります。

そのためユーザの操作に併せて
内部の状態の更新も行ってくれます。

一方で自身が管理しているデータ(いわゆるBacking Store)の更新(並べ替え)も
必要になります。

そこで今回新しく追加された
ReorderingHandlerを活用します。

https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/reorderinghandlers

この中のdidReorderで並び替えの結果を取得することができます。
その結果はNSDiffableDataSourceTransactionに入っています。

image.png

https://developer.apple.com/documentation/uikit/nsdiffabledatasourcetransaction

initialSnapshotには移動前の状態が
finalSnapshotには移動後の状態が入っており
differenceには移動前後で変化のあったItemの一覧が入っています。

NSDiffableDataSourceTransactionにはCollectionView全体の状態が含まれており
個々のSectionに関してはNSDiffableDataSourceSectionTransactionに含まれております。

https://developer.apple.com/documentation/uikit/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として使用している場合は
finalSnapshotidentifierをそのまま適用できます。

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です。

https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration

UICollectionLayoutListConfiguration
NSCollectionLayoutSectionの上に構築されています。

image.png

例えば下記のように使用した場合

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

image.png

ListsではこれまでUITableViewで実現していたレイアウトと
同じようなレイアウトを実現するためのオプションを用意しています。

  • .plain
  • .grouped
  • .insetGroup

さらにiPadOS14で登場したマルチコラムのレイアウト実現するために
新しく下記の2つのオプションも用意しています。

  • .sidebar
  • .sidebarPlain

image.png

https://developer.apple.com/documentation/uikit/uicollectionlayoutlistconfiguration/appearance

Separators Headers Footers

さらに
Listsでは
Separators Headers Footersの表示方法などを
詳細にコントロールすることができます。

image.png

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として使用しています。

image.png

この場合
画面にレンダリングされる際にViewを提供する
実装をする必要があります。

一番簡単な方法としては
Diffable Data Sourceの
supplementaryViewProviderを実装することです。

image.png

ただし
UICollectionViewDelegateを使っても
実装することができます。

(注意)各セクションにHeaderの設定が必要

1点気をつけなければならない点として
Headerを使用する場合
使用しないSectionにも設定が必要になります。

これを行わずnilをreturnすると
assertionエラーが起きます。

そのため下記のように.noneを明示的に設定する必要があります。

image.png

firstItemInSection

次にfirstItemInSectionです。

これはSectionの最初のセルをHeaderのように扱うように設定します。

image.png

この設定は
今回新しく登場した
階層構造を構築するようなレイアウトに使うことが
推奨されています。

ドキュメントにも下記のように書かれています。

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のサブクラスが
新しく追加されました。

https://developer.apple.com/documentation/uikit/uicollectionviewlistcell

これはListスタイルのレイアウトを実現するために
下記の項目に関して
多くのデフォルト設定が用意されており
さらに独自にカスタマイズできるようになっています。

image.png

個々の項目について見ていきます。

Separators

まずSeparatorsです。

image.png

これはよく見かけるようなレイアウトだと思いますが
例えばSeparatorをラベルの位置を合わせたいとします。
(ラベルとSeparatorのleadingを合わせたいとします。)

UITableViewの場合
Separator insetを調整することができます。

しかし

  • Safe area insets
  • Dynamic font size
  • SFSymbols

といったことを考慮し始めると
全ての場合において最適な状態にすることは難しくなります。

image.png

そこで
UICollectionViewListCellには
新しくSeparator Layout Guideが登場しました。

https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3601206-separatorlayoutguide

image.png

この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の一つの機能になっています。

そのため
セルの構築を行う際に一緒に設定をすることができます。

image.png

ただし
この機能はレイアウトとセルの間でのやりとりが必要になるため
list configurationを使用したセクションの中でのみ動きます。

image.png

独自のアクションを実装したい場合は
- leadingSwipeActionsConfiguration
- trailingSwipeActionsConfiguration

をOverrideすることで実現できます。

https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3601205-leadingswipeactionsconfiguration
https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3601207-trailingswipeactionsconfiguration

image.png

注意点

気をつけなければならない点としては
IndexPathを使用しないということです。

IndexPathはセルが挿入や削除されると位置がずれます。

仮にIndexPathでデータを管理している場合などは
間違ったデータの更新をしてしまう可能性があります。

image.png

そこでIdentifierを使用することで
そのリスクをなくすことができます。

image.png


本題とは関係ありませんが
UITableViewRowActionはiOS14でdeprecatedになりました。
https://developer.apple.com/documentation/uikit/uitableviewrowaction

Accessories

次にAccessoriesです。

UITableViewでは
- 扱える種類が少ない
- trailingにしか使えない

などの制約がありましたが
List Cellでは
かなり豊富な種類のAccessoriesを扱え
leading、trailing両方に設定できるようになりました。
さらにどちら側にも複数のAccessoriesを設定することが可能です。

また
UITableViewでは
Accessoriesは単なる装飾的な存在でしたが
List CellではAccessoriesを追加することで
Cellに機能を追加することができます。

https://developer.apple.com/documentation/uikit/uicellaccessory

例えば
このReorderを設定することで
ユーザがセルをタップをすると自動でCollectionViewがReorderモードになります。

image.png

DeleteをタップするとtrailingにSwipe Actionが表示されたり
image.png

Outline Disclosureはセクションのexpand collapseができるようになります。
image.png


ただし現在のXcode12.0 Betaですと
サンプルコードでは
上記3つの中で
disclosureIndicator以外は
変更しても表示されませんでした。

設定方法はセルのaccessoriesというプロパティに設定します。

https://developer.apple.com/documentation/uikit/uicollectionviewlistcell/3600968-accessories

image.png

表示方法ですが
デフォルトでは
UIKit側でどこに何を配置するのが最適かを
種類から判断し

自動で設定していると
セッションでは言っていました。


その基準がどうなっているのかまでは追えていないので
ご存知の方がいらっしゃいましたら教えてください?‍♂️

一方で様々に調整できるOptionも用意されいます。

例えば
下記ではEditモードではない場合にのみ
表示することができます。

https://developer.apple.com/documentation/uikit/uicellaccessory/displayedstate

image.png

位置に関しては下記のようなenumがあるので変更できるようです。
https://developer.apple.com/documentation/uikit/uicellaccessory/placement

今回紹介した以外にも様々なAccessoriesが用意されています。

例えばメールアプリではチェックマークが使用されています。

image.png

View Configuration

次にViewのConfigurationです。

image.png

これまではセルに直接値の設定をしていました。

image.png

これがConfigurationへ設定するように変わり
最後にcellのcontentConfigurationへ適用します。

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600949-contentconfiguration

image.png

これは一見すると同じように見えますが
contentConfigurationに設定することで

特定の種類のセルだけではなく
contentConfigurationに対応している全てのViewに
同じ設定を適用できるようになります。

image.png


ドキュメントで検索するとCellとHeaderFooterViewで利用できるようです。

スクリーンショット 2020-06-30 8.02.27.png

UIListConfigurationはデフォルトのレイアウトが用意されているため
少ない設定でCollectionViewに標準的なレイアウトを適用することができます。

2つのConfiguration

これまではList Content Configurationを見てきましたが
Configurationには2つの種類があります。

  • UIBackgroundConfiguration
  • UIListContentConfiguration

image.png

Background Configuration

Background Configurationには
セルのバックグラウンドに関連するプロパティの設定をします。

https://developer.apple.com/documentation/uikit/uibackgroundconfiguration

image.png

下記のように
backgroundConfigurationに設定します。

image.png

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600947-backgroundconfiguration

List Content Configuration

そして
List Content Configurationは
UIContentConfigurationに適合したUIListContentConfiguration
標準的なUITableViewのセルやHeader、Footerのレイアウトを提供します。

UIContentConfiguration
https://developer.apple.com/documentation/uikit/uicontentconfiguration

また
個々のプロパティも設定できるため
カスタマイズも比較的自由にできます。

image.png

上記でも見ましたが
下記が設定方法です。

image.png

推奨される使用方法

これらのConfigurationは
軽量でインスタンス生成のコストが非常に小さい
とセッション内では言っています。

また
Configurationはstructであり
cellに適用するまでは他に影響を与える心配がありません。

そこで推奨する使用方法としては
毎回defaultConfigurationメソッドから
新しいインスタンスを生成し
新しいconfigurationを設定していくのが良いと
セッション内では言っています。

こうすることで
古い状態に気にする必要もなくなりますし
複雑な状態や遷移処理が必要な場合にも
新しいconfigurationを毎回設定するだけになります。

さらに
Configurationはパフォーマンスにも良いと言っています。
これはUIKitと内部で連携することで
パフォーマンスの最適化が実現できていることによるようです。

Configuration State

次にConfiguration Stateです。

これは
セルやViewのレイアウトを設定する際に
現在セルがどのような状態にあるのかをプロパティとして保持しています。

これは先ほど見た
Configurationの出力結果です。

image.png

Configurationを利用することで
このような状態が異なる外見を
自動で利用できるようになります。

Configurationの実態は
特定の状態に応じた外見の記述です。

では
どういう状態があるのかを見ていきます。

Traits

まず一番大きい単位として
下記のようなUITraitCollectionの値があります。

image.png

States

次に
CellやViewがUIKitが内部に保持する状態があります。

image.png

Custom States

さらにその上に自身のカスタムな状態も実装することができます。

image.png

UIConfigurationState

これらの状態は
UIConfigurationState
を利用します。
https://developer.apple.com/documentation/uikit/uiconfigurationstate

具体的には
UIViewConfigurationState
UICellConfigurationState
を用いて保持します。

UICollectionViewUITableViewのどちらにも適用できます。

image.png

UIViewConfigurationState

https://developer.apple.com/documentation/uikit/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 
    }
}

https://developer.apple.com/documentation/uikit/uiconfigurationstatecustomkey

image.png

UICellConfigurationState

UICellConfigurationState
UIViewConfigurationState
にさらに

  • Editing
  • Swiped
  • Expanded
  • Drag and Drop

を追加した状態保持しています。

カスタムな状態の実装方法は同じです。

image.png

https://developer.apple.com/documentation/uikit/uicellconfigurationstate

Configurationの更新

updateConfiguration(using:)を用います。
https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration

まず現在の状態に応じたConfigurationを取得し
そのconfigurationに値を設定していきます。

image.png

このメソッドは直接呼ぶのではなく
setNeedsUpdateConfigurationを呼び出すことで間接的に呼び出すようにします。

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600432-setneedsupdateconfiguration

image.png

デフォルトでは
Configuration Stateが更新される度に
このメソッドが呼ばれ、
新しい状態に応じたConfigurationを自動で返します。

この動作を抑えるためには

automaticallyUpdatesContentConfiguration
automaticallyUpdatesBackgroundConfiguration
をfalseにします。

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600428-automaticallyupdatescontentconfi

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600427-automaticallyupdatesbackgroundco

image.png

使用例

これまで紹介したものと同じように
最初のConfigurationを取得し
状態に応じて設定の変更を行い
最後にconfigurationをセルに設定します。

image.png

UIConfigurationColorTransformer

上記の例でも
ステータスに応じてカラーを設定していましたが
UIConfigurationColorTransformer
を使ってよりカスタマイズしやすい形で設定することができます。

https://developer.apple.com/documentation/uikit/uiconfigurationcolortransformer

UIConfigurationColorTransformerはColorを引数に受け取り
別のColorに変換する一つのメソッドを持つstructです。

image.png

これを用いることで
同じColorから様々な状態に応じたColorを作り出すことができます。

Dynamic Layoutにも自動で対応

Dynamic Font Typeや
デバイスに応じたレイアウトにも自動で対応しています。

image.png

スクリーンショット 2020-06-30 17.50.37.png

layout Margins

Content Configurationは
MarginやPaddingを調整することができます。

この値と実際に表示される値から
intrinsic heightが計算され
Self-Sizingされます。

image.png

image.png

Image reserved layout size

最後にデフォルトの設定から
調整が必要な例が挙げられていましたので紹介します。

下記のレイアウトは
それぞれ異なるイメージですが
幅が一緒のイメージが使用されているおり
レイアウトに問題はなさそうです。

image.png

しかし
異なる幅の画像を使用すると
デフォルトの設定では
テキストは左揃えになりません。

image.png

これはイメージがleadingに揃える設定になっていることと
イメージとテキストの間のPaddingが同じ値になるように設定されていることが原因です。

これをテキストが左揃えになるように調整するために
reservedLayoutSizeを使用します。

https://developer.apple.com/documentation/uikit/uilistcontentconfiguration/imageproperties/3601000-reservedlayoutsize

すべてのセルに同じ幅を設定することによって
イメージがreservedLayoutSizeに合わせて
水平に真ん中寄せになります。

イメージの大きさには影響はありません。

image.png

既存の実装にConfigurationを追加時の注意点

いくつかConfigurationを使用する上での注意点が述べられていました。

Background configurationは項目をリセットする

Background configurationを使用すると
background colorやbackground viewはnilになるようです。

そのため既存の設定が反映されなくなる可能性があります。

image.png

Content configurationは値を上書きする。

Content configurationが保持しているプロパティで
- imageView
- textLabel
- detailedTextLabel

のようなセル、Header、Footerの
ビルトインのsub viewが保持している
プロパティは上書きされます。


内容と直接関係ありませんが
こういったプロパティを直接Viewに設定する既存の方法は
将来的にdeprecatedになるようです。

image.png

 他のViewと一緒にListsを利用する

UIListContentViewを利用することで
他のViewと一緒に使うことができます。
https://developer.apple.com/documentation/uikit/uilistcontentview

使い方としては
これまでと同様に
UIListContentConfigurationに値を設定して
UIListContentViewに反映し
これをSubViewとして追加すれば可能です。

image.png

UIListContentConfigurationが保持している
デフォルトのスタイルを利用しCustom Viewを簡単に作れるようになります。

UIListContentViewUIViewなので
CollectionViewTableView以外でも
利用可能です。

Custom Configurationを作成する

自身でConfigurationを作成することもできます。

その際は
UIContentConfigurationに適合させたstructを定義します。
https://developer.apple.com/documentation/uikit/uicontentconfiguration

そしてViewにはUIConentViewプロトコルに適合したViewを作成し
configurationプロパティに設定します。
https://developer.apple.com/documentation/uikit/uicontentview
https://developer.apple.com/documentation/uikit/uicontentview/3600985-configuration

Configurationを利用することで
Viewが新しい状態に変わった時の自動アップデートなど
多くのメリットを活用することができます。

image.png

全体的なUICollectionViewの構築例

最後になりますが
セッション内で紹介されていた
UICollectionViewの構築方法を見ていきます。

下記のようなレイアウトをリストを構築します。

image.png

レイアウト

Configurationの設定

まず
UICollectionLayoutListConfigurationを用いて
具体的なレイアウトの内容を設定します。

今回は.sidebarのスタイルを利用しています。

image.png

UICollectionViewCompositionalLayoutに設定

そして
UICollectionViewCompositionalLayoutlistメソッドに
引数として先ほどのUICollectionLayoutListConfigurationを渡すことで
垂直にスクロールするレイアウトが作成できます。

image.png

UICollectionViewに適用

最後にUICollectionView
UICollectionViewCompositionalLayoutを設定します。

image.png

データの設定

次にデータの設定です。
セルの内容と自身のモデルを繋げます。

この例では
- title
- image
を保持したMyItemと呼ばれるモデルを利用しています。

image.png

セルの登録

まずは利用するセルとモデルを
CellRegistrationを利用して登録します。

image.png

クロージャ内で
具体的にモデルをセルをどうやって設定していくのか
を定義します。

ここでは
defaultContentConfigurationメソッドで取得したインスタンスを
そのまま利用しています。

titleimageをcontentに設定し
最後にセルに設定を反映させます。

image.png

Data Sourceの生成

データとセルは
Data Sourceを経由して繋がります。

表示が必要になった際に
Data Sourceのクロージャで定義された方法で
セルを構築していきます。

image.png

今回はUICollectionViewDiffableDataSourceを利用しています。

Section TypeとItem Typeを設定して
Diffable Data Sourceを生成します。

image.png

Diffable Data Sourceは
具体的なCollectionViewを引数に受け取り初期化します。

セルを構築する際には
クロージャで
collectionView, indexPath, itemを受け取って
構築していきます。

image.png

クロージャではCellを返します。
dequeueConfiguredReusableCell
先ほど作成したCellRegistrationを渡します。

これだけでセルの再利用などは
フレームワークが自動で管理してくれるようになります。

image.png

結果

最終的に下記のようなリストが構築できます。
セルの状態に応じてbackground colorなども
自動で変更されるようになっています。

image.png

appearanceに.sidebarPlainを設定すると
下記のようなレイアウトになります。

image.png

まとめ

今年のアップデートを見ていきました。

昨年に引き続き
たくさんのアップデートがありました。

特に今年はUITableViewにとって変わるような
Listsが登場したことが大きく印象に残っています。

デフォルトで必要な設定は何もせずに利用できることで
コードを短くすることができる上に
カスタマイズができる余地も与えている点が
非常にバランスが取れているのではないかと感じています。

そしてUITableViewは
今後なくなっていくのではないかと思いました。
(セッションの中でも置き換えることを推奨しているような発言がありました)

もう一つ気になった点としては
Section Snapshotで
SectionSnapshotHandlers
ReorderingHandlers
といったstructが用いられていた点です。

これまでUIKitではdelegateパターンを用いて
ユーザのアクションなどを受け取る設計が多く見られましたが

今回のようなstructを利用することで
定義とハンドリング処理を同じ場所で
より宣言的にハンドリング処理を記載できるなど

さらに見やすくなったのではないかなと個人的には思いました。

より具体的にどういう風に利用するのかを
見たい方は公式のサンプルコードがおすすめです。

これを見れば
今年の更新内容や
昨年登場したDiffableDataSourceCompositionalLayout
の使い方も把握できると思います。

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

UICollectionView
どんどん進化していっているので
今後もアップデートが楽しみです?

間違いなどございましたらご指摘頂けますと嬉しいです?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む