忙しい人向けの Structure your app for SwiftUI previews - #WWDC20

忙しい人向けシリーズの第20弾。

冒頭

Xcode プレビューは、SwiftUI のコードを書いたり、一度に複数のプレビューをしたり、コードの変更を素早くテストするのに役立つ。

しかし、アプリを構造化してプレビュー可能にすることで、より理解しやすく、よりテストしやすく、より保守しやすいものになり、アプリ全体に利益をもたらすものとなる。

このセッションでは、アプリを構造化してプレビューを最大限に活用するための4つの方法を紹介していく。

画像60

Previewing multiple files

最初は複数のファイルを一度にプレビューして、より大きなコンテキストで編集する方法。

画像1

このセッションを通して使用するサンプルアプリは、写真のコラージュをするもの。

画像5

あるプレビューを表示させたまま、特定のファイルを編集したい場合がある。そうした際は、プレビューエリア左下の 📌 をクリックしてピン留めしてから別のファイルを開くと・・・

画像6

以下のように2つのプレビューが同時に表示できる。

画像7

なお、ダークモードの確認もしたくなったらボタンで複製してから、インスペクタの Color Scheme から Dark を選択すればよい。

画像8

また、ここでは Asset カタログに事前に登録しておいた色に変更したいが、それは Library の Color タブから選択できる。

画像9

ピン留めは Asset カタログの編集時などにも活用できる。

画像10

ピン留めのもう一つの活用方法として、開発中にずっと必要なプレビューを専用のファイルとして用意し、それをピン留めしてずっと表示させておくというアイディアもある。

画像11

ここまでのピン留めの活用方法をまとめると。

・より大きなコンテキスト内での編集
・Swiftファイル以外の編集
・特定のタスクのための追加プレビュー

App life cycle

次に iOS 14 / iPadOS 14 から導入されたライフサイクルについて。

画像2

アプリの起動時には、コストのかかる初期化処理をすることが多い。SwiftUI のプレビューではこのような高価な処理をしたくない。

画像12

サンプルアプリでは、ネットワーク用のモデルをプロパティとして保持しており、アプリ起動時に初期化されるようになっている。プレビューを実行した時のアプリの動作は Debug Preview を実行することで確認できる。これはブレークポイントを設定してデバッグするのに便利だし・・・

画像13

今回のケースではデバッグゲージを開いて、アプリの動作を確認するのにも使える。この例では、プレビュー実行時にもかなりのリソースが使われているのが分かる。

画像14

SwiftUI に用意された @StateObject という Property-wrapper を活用することで解決できる。これはアプリ実行時にのみ初期化されるもので、プレビュー実行時には初期化されない。

画像15

先程に比べて、ずっと低リソースでプレビューできているのが分かる。

画像16

@StateObject の詳細については、App Essentials in SwiftUI のセッションも参考のこと。

画像17

しかし、データが初期化されないとしたら、どうやってプレビューにデータを取り込めばよいのか?2つのパートにわけて解説していく。

Sample data

まずは「どこに(where)」データを定義するのか。

画像3

このサンプルアプリでは写真を扱うため、開発中は多くの写真を用意しておきたいが、これは製品版には含めたくない。

画像18

プロジェクト設定の最下部にある Development Assets を利用することでそれを実現できる。ファイルやフォルダのパスで指定することができ、それらは開発中のバージョンのみに含まれるようになる。

画像19

SwiftUI のテンプレートを使用してプロジェクトを作成した場合、デフォルトで設定されているが、独自のものを追加することもできる。

画像20

なお、アセットだけでなくソースファイルも対象となるので便利。

画像21

Structuring views

画像4

アプリには CoreData や CloudKit などを活用したリッチなデータモデルが含まれるが、最終的にユーザと対話する際は String / Bool などのシンプルなデータ型を利用することになる。

画像23

その中間には多数の View が存在することになるが、これらの階層のどこでデータ変換を行うべきなのか?

画像24

一般的な経験則としては、リッチデータ型からシンプルデータ型への変換が早ければ早いほど、再利用性、テスト性、プレビュー性が向上する。

画像22

プレビュー可能にするための4つの例を見ていく。

画像25

1. Immutable inputs

最初は Immutable なデータを渡す例。以下において、セルは Collaborator という観測可能(Observable)なオブジェクトを受け取って構築される。

画像27

プレビューを作成するために、CKUserIdentity の指定が必要であると分かったが、CloudKit のデータモデルは先程のデモでオフにしてしまっている。

画像26

その View に渡す必要のあるデータの最小量を特定することが大切。今回は3つのプロパティだけで十分なので次のようなコードになった。

画像28

これはプレビューを作成するのも簡単。

画像29

複数のコンフィギュレーションを適用するのもとても簡単。

画像30

しかし、シンプルすぎるデータ型を選んでしまう場合がある。主に日付などのユーザのロケールに基づいてフォーマットするものなど。

ここではユーザ名の表示に String 型の代わりに PersonNameComponents 型を渡すように変更した。文字列のインターポレーションでフォーマッタを指定することができる。

画像31

2. Mutable inputs

次に View が変更できるシンプルなデータ型として、SwiftUI のバインディングを使った例を見ていく。

画像32

このインスペクタでは ImageSlot という観測可能なオブジェクトを受け取っているが、その中身を見ると・・・

画像33

backingRecord が CloudKit での動作を前提にしてしまっている。

画像34

先程と同様に View が必要としている最小のデータを渡すようにしたバージョンを見てみる。ここでは SlotEffects のバインディングを渡すようにしている。SlotEffects 型は浮動小数点の集まりでシンプルなデータ型。

画像36

このようにプレビューも簡単に作れる。ここでは .constant() を利用して定数バインディングにしているため値の変更はできない。しかし、中間のデータ構造を作ることで動的なバインディングを渡すこともできる。

画像35

ここでは EffectsContainer という中間データ型を作成している。これには @State で SlotEffects を保持しており、それを $effects でバインディングとして渡している。

画像37

プレビューを Live mode にしてスライダーを動かしてみると、実際に値が変更できているのが分かる。

画像38

もう1つ、この View には写真を置換するボタンを追加したい。必要なすべてのデータを渡してロジックをここに記述する方法も考えられるが、ボタンをタップした時の挙動はクライアントに決めてもらいたい。

そのような時には、ボタンをタップした時に実行するクロージャを受け取ることで解決できる。

画像39

プレビューでクロージャを渡してみると、ボタンタップ時に正しく呼び出されることが確認できる。

画像40

プレビューは実機上でも実行できる。Preview on Device ボタンから iPhone XR を選択してみると・・・

画像41

同じようにプレビューが表示され操作できるようになる。標準のプレビューと同様に、コードを変更するとそれがシームレスに反映される。

画像42

ホーム画面には Xcode previews というアイコンが表示されるようになる。これは最後に実行したプレビューを保持しているため、Mac から切断して同僚などに見せにいくこともできる。

画像43

3. Generic inputs

3つ目として、メインエディタにデータを渡すためにジェネリクスを利用する例を見ていく。 

画像44

データモデルを見てみると、CKShare という CloudKit に依存したデータを保持している。

画像45

そのため、前回までと同様に必要なデータをバインディングとして渡す方法を利用してみる。ここでは名前・レイアウト・スロットを渡しているが・・・

画像46

ImageSlot は前回のデモで見たようにリッチなデータ型なので、プレビュー用のデータを作成するのが困難であることに気づく。

画像47

今までと同様に必要最小限のデータを渡す方法を考えてみる。ここで必要となるのは Protocol を使った抽象化

ここでは ImageSlot の代わりに、SlotProtocol という Protocol に準拠したデータ型を受け取るようにした CollageProtocol を定義した。これによってプレビュー用のデータモデルが作れるようになる。

画像48

プレビュー用として CollageProtocol に準拠した DesignTimeCollage を作成した。名前・レイアウト・スロットを持っており・・・

画像49

DesignTimeImageSlot は SlotProtocol に準拠して次のように定義されている。

画像50

プレビュー対象となるエディターは、次のように2つのジェネリックパラメータで構成されている。CollageProtocol は先ほど見たもので、もう1つの ImagePickerType はデザイン時に簡単に写真選択できるようにするためのもの。

画像51

プレビューは以下のように定義できる。PhotoPicker を作るためのクロージャには、プレビュー用として先ほど見た Asset カタログからデータを取得する DesignTimePhotoPicker を生成する関数を渡している。

画像53

これでエディタは完全な機能を持つようになった。Live Preview を実行するとキャンバス上で一通りの操作ができる。

画像53

スロット用のデータを渡すこともできる。画像については Asset カタログから読み込み、それを Publisher に変換して渡している。

画像54

iPad での横向きでの確認も Preview on Device の機能によって、インタラクティブに行える。

画像55

4. Environment inputs

最後に他とは少し違う例を見ていく。基本的にデータは依存関係を明示的に入力として受け取ったほうがいいが、環境(Environment)として受け取ると便利な場合がある。

画像56

ここではクラウドとの同期状況を表す CloudSyncStatus を @EnvironmentObject で受け取っている。SwiftUI ではそれを受け取るためには、View 階層のより上位で環境を設定しておく必要がある。

画像57

プレビューでは、各 View に environmentObject モディファイアを利用して指定することで実現できる。

画像58

他の例と同じように、複数の設定での確認もかんたんに行える。

画像59

振り返り

このセッションでは多くのことを見てきた。振り返ると以下のようになる。

【Previewing multiple files】
最初に、より大きなコンテキストで View を編集できるように、ピン留めによって複数のファイルを同時にプレビューする方法を見た。

【App Lifecycle】
2つ目に、プレビューと SwiftUI アプリのライフサイクルを理解し、必要なときだけ初期化されるように @StateObject を利用する方法を見た。

【SampleData】
3つ目に、サンプルデータを定義する場所と、Development Assets を利用して開発時だけに有効なデータを用意する方法を見た。

【Structuring views】
最後に、View をよりプレビュー可能(pre-viewable)にする4つの方法を見た。

画像60

最後に

1つの大きなアイディアとして、アプリをよりプレビュー可能(pre-viewable)にする投資はアプリ全体に利益をもたらす。

このセッションを見たことで、プレビュー可能なアプリは、より理解しやすく、よりテストしやすく、より保守しやすいアプリであると理解してもらえただろうか。 

関連

忙しい人向けの App Essentials in SwiftUI

免責

・本記事は公開情報のみに基づいて作成されています。
・要約(意訳)のみなので、詳細はセッション動画をご確認ください。


役に立った記事などありましたらサポート頂けると嬉しいです。