アイコンの出典:https://icons8.com
こんにちは、株式会社スタメンでiOSエンジニアをしている青木 (@38Punkd)です。
先日の投稿にあった通り、スタメンは iOSDC Japan 2023 にゴールドスポンサーとして協賛します。私はそのスポンサーセッション枠として登壇します。この記事では、当日発表する内容を少し先出ししてご紹介できればと思います。
私たちは TUNAG というプロダクトを、Web・iOS・Androidの3プラットフォーム上で提供しています。iOSアプリではVIPERアーキテクチャを導入しています。具体的な導入方法については、弊社で以前に試行錯誤した様子が以下の記事で解説されていますので、気になる方は読んでみてください。
TUNAG iOSアプリのサポート対象バージョンの下限を、今年からiOS 13に変更したので、13から使用可能になる SwiftUI の導入を本格的に検討しました。
実際にSwiftUIを導入するに至ったのですが、これまでVIPERで構築してきたアプリに対して、SwfiftUIを導入する上で何を具体的に検討したかをご紹介します。
SwiftUIとは
2019年にApple社が提供開始した宣言的UIフレームワークのことです。XcodeのGUIでSwiftUI
を選択してファイルを新規作成すると、以下のようなViewが初期生成されます。
struct SwiftUIView: View { var body: some View { Text("Hello World!!") } }
SwiftUIには2つの大きな特徴があります。
- データバインドの仕組みを持つ
- UIKitと互換性がある
それぞれについて簡単に補足します。
データバインドの仕組みを持つ
SwiftUIは先のコードで示したように、画面に表示するViewを構築する部分をView
プロトコルに準拠した構造体で表現します。
SwiftUI Viewの画面更新をするには、ObservableObject
プロトコルに準拠した、状態管理用クラスを用意します。
これがいわゆるViewModelに相当し、このViewModelの値が更新されるとSwiftUI Viewの該当のViewが自動更新されるため、SwiftUIはそれ自体がMVVMの特性を持ちます。
UIKitと互換性がある
- SwiftUIのViewをUIKit上で使うためのラッパークラス UIHostingController
- UIKitのViewをSwiftUI上で使うためのラッパークラス UIViewRepresentable
が用意されています。
どちらのオブジェクトも、UIKit・SwiftUIをインポートすれば、iOS 13以上ならどこからでも呼び出せるため、SwiftUIはUIKitと互換性が非常に高いと言えます。
実際にVIPERにSwiftUIを導入する上で検討したこと
これまでUIKitを用いてUIViewController
上でViewを構築してきたので、SwiftUIを導入することでアンコントローラブルな点が出てこないか、また、データバインドの仕組みを持つSwiftUIがVIPERに馴染めるか等の相性を考慮する必要があります。
この相性について大きく分けると、以下2点を考慮する必要があります。
- UIViewControllerがSwiftUIのライフサイクルをハンドリングできるか
- UIViewControllerがSwiftUIのイベントをハンドリングできるか
1点目について、詳しく説明します。
1. UIViewControllerがSwiftUIのライフサイクルをハンドリングできるか
“SwiftUIのライフサイクル”は、さらに3つの具象に分けて考えられます。
1-1. SwiftUI View自身のライフサイクル
1-2. SwiftUI ViewのデータバインドをするObservableObjectプロトコルのライフサイクル
1-3. SwiftUI ViewをUIKitで使うためのラッパーUIHostingControllerのライフサイクル
1つ目について、深掘ります。
1-1. SwiftUIのView自身のライフサイクルをハンドリングできるか
Viewのライフサイクルとは、Viewがインスタンス生成された後に画面として使われ、その後インスタンス破棄されるまでの一連の流れのことを指します。
UIViewControllerのライフサイクルに対して、UIKitでは多くのメソッドが提供されています。
出典:https://developer.apple.com/documentation/uikit/uiviewcontroller
対してSwiftUIでは、viewDidAppear
とViewDidDisappear
に相当する、onAppear
とonDisappear
メソッドの2つだけが提供されています。
しかし、このonAppearとonDisappearメソッドは、いくつかのケースにおいては呼ばれないことがあります。
また、ある画面のライフサイクルメソッドを呼ぶには、その画面への遷移ロジックが必要です。iOS 15までは、UIKitのUINavigationController
に相当するSwiftUIのNavigationView
が、SwiftUI View間の画面遷移ロジックを担います。(iOS 16からは、NavigationStack
も使えるようになります)。
このNavigationViewは、挙動が比較的不安定であると言われています。
これらを踏まえて、ライフサイクルメソッドを呼ぶための画面遷移ロジックは、現時点ではSwiftUIの機構に任せるのではなく、UIViewControllerに任せる判断をしました。
続いて、SwiftUI Viewはインスタンス生成のタイミングについてはどうでしょうか。SwiftUI Viewが準拠するViewプロトコルには、id
メソッドがデフォルト実装されています。
出典:https://developer.apple.com/documentation/swiftui/view/id(_:)
SwiftUI Viewがインスタンス生成されるタイミングは、自身の保持するIDが生成・更新された時だと分かります。
以上から、SwiftUIのView自身のライフサイクルをハンドリングをするには、3点を考慮すれば良いと言えます。
- UIViewControllerに比べて、SwiftUI Viewのライフサイクルメソッドの種類は少ない、かつ完全にイコールではない
- 画面遷移ロジックは、SwiftUIの機構は使わず、UIViewControllerに任せる
- SwiftUI Viewのインスタンス生成のタイミングはidの変化をトリガーにすると良い
つまり、画面遷移ロジックはUIViewControllerに任せて、SwiftUI Viewの責務は、見た目の部分のみを担わせた方が良さそうと言えます。
続いて、先ほど列挙した3つの具象のうち、2つ目についてです。
1-2. ObservableObjectプロトコルのライフサイクルをハンドリングできるか
SwiftUI でViewの状態管理をするには、ObservableObjectプロトコルに加えて、@StateObject
や@ObservedObject
を用います。
どちらも、ObservableObjectプロトコルに準拠しているクラス内の値変化を、監視できるようにするためのプロパティラッパーです。
両者の違いは、
@StateObjectは、保持するViewに対して、インスタンスが1つだけ生成されるのに対して、
@ObservedObjectは、保持するViewのライフサイクルに依存し、親Viewの再描画でインスタンスは再生成されるという点です。
私たちのプロダクト TUNAG は、iOS 13からサポート対象ですが、@StateObjectはiOS 14からしか使えません。なので、@ObservedObjectを使うことが必須になります。
結論としては、@ObservedObjectは親Viewのライフサイクルに依存するので、画面単位のライフサイクルと一致させると、ハンドリングしやすいという考えに至りました。
すなわち、@ObservedObjectを保持するクラス = UIViewController とすれば、ObservableObjectプロトコルのライフサイクルをハンドリングしやすいと言えます。
最後に、3つ目についてです。
1-3. SwiftUI ViewをUIKitで使うためのラッパーUIHostingControllerのライフサイクルをハンドリングできるか
これについては、UIHostingControllerはUIViewControllerを継承しているので、ライフサイクルを親ViewControllerに委譲(didMove
メソッドで可能)できます。
出典:https://developer.apple.com/documentation/swiftui/uihostingcontroller
つまり、親ViewControllerのライフサイクルによって、SwiftUI Viewのライフサイクルをコントロールできます。
ここまで、UIViewControllerがSwiftUIのライフサイクルをハンドリングできるかについて、説明してきました。
では、SwiftUIのイベントハンドリングについてはどうでしょうか。
UIViewControllerがSwiftUIのイベントをハンドリングできるか
SwiftUI View内で発生するイベント、例えばボタンタップは、タップ時の処理を記述できます(当たり前と言えば当たり前ですが)。
この処理を別のクラスに伝播させれば、SwiftUI Viewのイベントをハンドリングできます。具体的には、以下のように完了ハンドラや、デリゲート等でハンドリングできます。
struct ViewScreen: View { var tapHandler: (() -> Void)? var body: some View { Button { tapHandler?() } label: { Text("Tap Me !!") } } }
まとめ
SwiftUIの性質を分けて、VIPERにSwiftUIを導入する上で検討した事をご紹介しました。
UIViewControllerがSwiftUIのライフサイクルをハンドリングできるか
1-1. SwiftUI View自身のライフサイクル
1-2. SwiftUI ViewのデータバインドをするObservableObjectプロトコルのライフサイクル
1-3. SwiftUI ViewをUIKitで使うためのラッパーUIHostingControllerのライフサイクル
UIViewControllerがSwiftUIのイベントをハンドリングできるか
以上を踏まえて、最終的にSwiftUIを導入する上での要件を定めました。
- 画面遷移をUIViewControllerに担わせるため、UIViewController.view = SwiftUI View の構造を作る
- ObservableObjectに準拠したクラス(ViewModel)のインスタンス(@ObservedObject)をUIViewControllerに保持させる
この2つの要件を元に、実際にVIPERにSwiftUIを導入する方法をコードで示せればと思いますが、
ここからは iOSDC Japan 2023 当日、以下スケジュールでご紹介できればと思います!!
登壇・出展情報
- 日時:2023年9月3日 15:05〜(20分)
- 場所 : 早稲田大学 理工学部西早稲田キャンパス
- Track:C
当日はスポンサーセッションだけでなく、ブースも出展しますので、気軽に遊びに来てください!
当日お会いできる事を楽しみにしています!!
採用情報
スタメンでは、iOSエンジニアに限らず全技術領域で、プロダクトを成長させていくエキスパートを募集しています。もし興味を持っていただけたら、下記からご応募ください!