Pro SwiftUI

 

Preface

よく使うコマンド

Chapter 1. Layout and Identity

Parents and children

SwiftUIのレイアウトプロセスは3ステップ
  1. 親ビューが子にサイズを提案する
  1. その情報に基づいて子はサイズを選択する。親はそのサイズを尊重する
  1. 親ビューが子を配置する
 
下記のコードのTextビューの親は何?と尋ねたら「VStack」と答えたくなるが、正解であり間違いでもある
 
.frame()などのmodifierは、実際は親ビューに当たる を生成している
  • ビューは自身の位置を決定することができない
    • Text()は文字の領域に合わせたビューのみが生成されていて、中央寄せなどの位置調整は親の が決定している
  • 全ての修飾子が新しいビューを作成するわけではない

Fixing view sizes

レイアウトを決定する6つの値
  • Minimum width and minimum height
    • ビューが許容する最小スペース
    • これより小さい値は無視され、ビューが想定されたスペースからはみ出す
  • Maximum width and maximum height
    • ビューが許容する最大スペース
    • これより大きい値は無視され、親ビューは残りのスペース内にビューを配置する
  • Ideal width and ideal height
    • このビューに必要なスペース
    • 最大値から最小値までの範囲内で指定できる
 
はビューの理想的なサイズを最小サイズと最大サイズに昇格させる
は理想的なサイズを変更しない

Layout neutrality

他のビューとどう組み合わせて使用するかに応じて自動的にサイズ調整することを「Layout neutrality」という
6つのサイズ設定値はoptionalで、Layout neutralityと固定値を切り替えることができる

Multiple frames

単一のビューは、固定のサイズor可変サイズを持つことができるが、両方を持つことはできない
 
Q: frame(width,height)は6つの値を持ってない?特殊なビュー?

Inside TupleView

アンダースコアがついていない定義はpublic API
TupleViewはビューを10個までしか受け入れられない(自分で定義すれば拡張できる)

Understanding identity

idは2種類ある
  • An explicit identity:明示的に指定するid
  • A structural identity:暗黙的に生成されるid
 
明示的なidが必要な場面は2つ
  1. 配列のループ処理など動的なデータを処理している場面
  1. 特定の場所までスクロールするなど、特定のビューを参照する場面
 
Q: Tree diffingが分からない
 
Swiftコンパイラでは、使用しているすべてのサブビュー、すべての修飾子、すべての条件、すべてのループなどを、ビューの型に直接エンコードしている
e.g.
if文だけじゃなく、switchも同様に の組み合わせでできてる
 
@ViewBuilderがあるとレイアウトを型に変換する
bodyには元々ついている
 
ビューのidがビューの有効期間を決定する
idが変更されるとビューは破棄されて再生成される
  • パフォーマンス観点でかなり悪いこと
  • ビューが保持している値も破棄される
  • アニメーションもできないのでトランジション(フェード)になる
 
SwiftUI はビューのbodyを評価するたびに、その内部にあるすべてのビューの新しいインスタンスを作成する
 
プロパティを使うとビュー全体のインスタンス更新を防ぐことができる
三項条件演算子を使うといい
修飾子の中には、カスタマイズパラメータの受け取りを拒否するものがある。hidden()とか
self.opacity(hidden ? 0 : 1)で回避できる

Intentionally discarding identity

animationさせたくないようなケースでは、ランダムなidを明示することで別の要素として扱ってもらうことができる
当然再構築が発生するのでパフォーマンスには気を付けて

Optional views, gestures, and more

オプションは便利

Chapter 2. Animations and Transitions

Animating the unanimatable

明示的なアニメーション
暗黙的には を使う
 
通常アニメーション化できないものでも、を継承したクラスを定義して
modifier化すると実現できる
 
この種の制御は、iOS 16未満のバージョンをサポートする必要がある場合に特に重要です。iOS 15.6以下では多くの要素がアニメーション化できないため

Avoiding pain in iOS 15.6 and below

はiOS 15.6以下でアニメーション化できないが、 で近いことができる

Creating animated views

は間の値を補完するだけのシンプルな機能なので、アニメーション以外にも使える
VoiceOverを多用している人や、アプリにアニメーションを減らすよう指示している人もいるので、オプションを用意しておくと良い

Custom timing curves

カスタムタイミングカーブは自分で作成できる
こういうサイトでいい感じのカーブが作れる

Overriding animations

でユーザーの設定を取得できる
 
こういうグローバル関数を定義しておくと、 を直接呼んだ時と を呼んだ時で期待挙動を制御できる(後者は設定次第でアニメーションしない場合がある)
 
版も

を使うことで上書きもできる を使うと、そのビューに関連するTransactionだけを制御できる

Advanced transitions

シミュレータの [デバッグ] → [スロー アニメーション] をオンにするとアニメーションの調整がしやすい
0.0 にスケーリングしようとするとエラーになるため、0.00001 のような小さな値にしておくとよい

Want to go further?

こういうmodifierを定義するとグラデーションを実現できる
 

Chapter 3. Environment and Preferences

The environment

などは新しいビューを作成せずに要素に変更を加えられる
VStackなどにmodifierをつけても子のTextに伝搬される
これは Enviroment機能を使っている
 
こんな感じで定義できる
 
拡張機能にしておける
 
描画側
 
CirclesViewを呼び出すコード
 
って命名が予約語っぽくて紛らわしい
 
参考

@Environment vs @EnvironmentObject

 
EmvironmentKeyはビューの更新はやらない
 
可能な限りはEnviromentKeyを使った方がいい
  1. デフォルト値が提供されるから
  1. ObservableObjectは利用するすべてのビューを更新するから
    1. @Enviromentで紐づけておけばビューは更新されないけど環境キーは更新される
    2. 参考している値が変わってない場合でも ObservableObjectが変わるたびに更新されてしまい、無駄に負荷が高くなる
 
ObservableObjectとEnviromentKeyを併用して、全体にテーマを適用しつつ、キー自体はEnvriomentKeyで必要な場合のみ更新させる、みたいなこともできる
 
また、キーパスを指定して監視することもできる

Overriding the environment

でenvironmentを上書きできる

Preferences

はデータが子ビューから上位ビューに流れている
複数の値が流れてくる場合は、どの値を使うかはmodifierによって異なる( を使う)
複雑になるので使い方には注意が必要
を使うことで自分でも実装できる

Anchor preferences

でアンカーを取り出して指定した に設定できる

Chapter 4. Custom Layouts

Adaptive layouts

  • AnyLayoutビューはレイアウトを動的に切り替えることができる型消去ラッパー
  • VStack, HStack, ZStackでなく、VStackLayout, HStackLayout, ZStackLayoutを使う
    • これらはLayoutプロトコルに準拠していて、独自クラスを作ることもできる
  • GridLayoutにも切り替えられる
    • GridRowはGrid以外の内部にある場合は無視される

Implementing a radial layout

 
  • 円周上にサブビューを配置するレイアウトの作り方
  • Layoutプロトコル
      • レイアウトの提案サイズと、その中んにあるすべてのサブビューが渡される
        • 未指定の場合もあれば、高さだけ渡される場合もある
      • 実際に必要なサイズを返す
      • 各サブビューを配置する位置を決める
      • 最終的は をコールする
 
 
  • は指定された領域もしくは最低限10ptのCGSizeを返す
    • : 未指定
    • : 最大スペース
    • : 最低限のスペース
  • 三角関数を使っていい感じに座標計算をする
    • anchorを.centerにしているので、サブビューの半径を計算に含める必要がある

Implementing an equal width layout

  • 等幅レイアウトの作り方
    • 通常のHStackではサブビューごとにそれぞれ横幅が割り振られるので等幅にならない
  • サブビューの中から最大の横幅を見つけ、要素数を掛けて全体のサイズを導き出す
  • スペースにも考慮が必要
    • SwiftUIでは明示しない場合に自動的にスペースが割り振られる(プラットフォーム毎に変わる)
    • スペース分を取得して計算に含める必要がある

Implementing a relative width layout

 
  • 各ビューに相対的なサイズを割り当てられるレイアウトの作り方
  • で指定できる
    • priorityの数値を合計して、全体の内で占める割合に応じて各サブビューのサイズを決定する

Implementing a masonry layout

  • 石積みレイアウト(ウォーターフォールレイアウト)の作り方
    • 列はあるけど行はない、不規則なグリッド
  • 今までのカスタムレイアウトの組み合わせで実現できる
  • ループの中で、次の要素は一番短い列に収納するようにする
  • はビューの軸に合わせて水平・垂直を決定する
    • デフォルトは垂直レイアウトを想定する
    • カスタマイズする場合は を定義する
 

Layout caching

  • 複雑な計算が必要な場合はキャッシュを検討できる
    • 絶対やらないといけない訳ではなく、パフォーマンスに問題あるときだけでok
    • バグの原因になりやすいから、必要な時以外はやらない方がいい
  • の引数に があるので、Voidを自分で定義したキャッシュの型に書き換える
  • 使おうとしているキャッシュが今のレイアウトに適しているかチェックが必要(横幅が変わった場合など)

Customizing layout animations

  • SwiftUIのアニメーションは、始点から終点に向かって直線的に移動する
  • animatableDataを定義すると、が毎回呼ばれるようになり、円周上をアニメーションするようになる
    • 元々は始点と終点の2回分しか呼ばれていない
    • 呼び出しが増えているのでパフォーマンスには注意(必要に応じてキャッシュを使おう)
 
 

Chapter 5. Drawing and Effects

    Chapter 6. Performance