VIPERアーキテクチャ採用のTUNAG iOSアプリにSwiftUIを導入しました

見出し画像

アイコンの出典:https://icons8.com

こんにちは、株式会社スタメンでiOSエンジニアをしている青木 (@38Punkd)です。

先日の投稿にあった通り、スタメンは iOSDC Japan 2023 にゴールドスポンサーとして協賛します。私はそのスポンサーセッション枠として登壇します。この記事では、当日発表する内容を少し先出ししてご紹介できればと思います。

fortee.jp

私たちは TUNAG というプロダクトを、Web・iOS・Androidの3プラットフォーム上で提供しています。iOSアプリではVIPERアーキテクチャを導入しています。具体的な導入方法については、弊社で以前に試行錯誤した様子が以下の記事で解説されていますので、気になる方は読んでみてください。

tech.stmn.co.jp

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の特性を持ちます。

SwiftUIを利用したMVVMアプリの構造

UIKitと互換性がある

が用意されています。

どちらのオブジェクトも、UIKit・SwiftUIをインポートすれば、iOS 13以上ならどこからでも呼び出せるため、SwiftUIはUIKitと互換性が非常に高いと言えます。

実際にVIPERにSwiftUIを導入する上で検討したこと

これまでUIKitを用いてUIViewController上でViewを構築してきたので、SwiftUIを導入することでアンコントローラブルな点が出てこないか、また、データバインドの仕組みを持つSwiftUIがVIPERに馴染めるか等の相性を考慮する必要があります。

この相性について大きく分けると、以下2点を考慮する必要があります。

  1. UIViewControllerがSwiftUIのライフサイクルをハンドリングできるか
  2. 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では多くのメソッドが提供されています。

UIViewControllerのライフサイクル

出典:https://developer.apple.com/documentation/uikit/uiviewcontroller

対してSwiftUIでは、viewDidAppearViewDidDisappearに相当する、onAppearonDisappearメソッドの2つだけが提供されています。

しかし、このonAppearとonDisappearメソッドは、いくつかのケースにおいては呼ばれないことがあります。

また、ある画面のライフサイクルメソッドを呼ぶには、その画面への遷移ロジックが必要です。iOS 15までは、UIKitのUINavigationControllerに相当するSwiftUIのNavigationViewが、SwiftUI View間の画面遷移ロジックを担います。(iOS 16からは、NavigationStackも使えるようになります)。

このNavigationViewは、挙動が比較的不安定であると言われています。

これらを踏まえて、ライフサイクルメソッドを呼ぶための画面遷移ロジックは、現時点ではSwiftUIの機構に任せるのではなく、UIViewControllerに任せる判断をしました。

続いて、SwiftUI Viewはインスタンス生成のタイミングについてはどうでしょうか。SwiftUI Viewが準拠するViewプロトコルには、idメソッドがデフォルト実装されています。

Apple公式リファレンスの、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メソッドで可能)できます。

Apple公式リファレンスの、UIHostingControllerについての説明

出典: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を導入する上で検討した事をご紹介しました。

  1. UIViewControllerがSwiftUIのライフサイクルをハンドリングできるか

    1-1. SwiftUI View自身のライフサイクル

    1-2. SwiftUI ViewのデータバインドをするObservableObjectプロトコルのライフサイクル

    1-3. SwiftUI ViewをUIKitで使うためのラッパーUIHostingControllerのライフサイクル

  2. 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

当日はスポンサーセッションだけでなく、ブースも出展しますので、気軽に遊びに来てください!

当日お会いできる事を楽しみにしています!!

tech.stmn.co.jp

採用情報

スタメンでは、iOSエンジニアに限らず全技術領域で、プロダクトを成長させていくエキスパートを募集しています。もし興味を持っていただけたら、下記からご応募ください!

herp.careers