SwiftUIでコンポーネントを作成し、UIKitで画面遷移をするための基本戦略 #TimeTreeアドカレ
これは株式会社TimeTree Advent Calendar 2023の17日目の記事です。
こんにちは。TimeTreeのiOSエンジニアの @pengtaros です。
TimeTreeのiOSアプリのUI開発にはUIKitとSwiftUIの両方を採用しています。
この記事ではSwiftUIを採用した経緯と共に、UIKitとSwiftUIをどうミックスして使用しているかを紹介します。
対象読者
- UIKitベースのアプリを開発していてSwiftUIも使ってみたいなと思っている方
- UIKitとSwiftUIをミックスして開発する場合の開発効率の改善に興味がある方
- TimeTreeのiOSアプリの開発手法について興味がある方
SwiftUIの利点
TimeTreeがSwiftUIを採用した理由は、一にプレビュー、二にプレビュー、三、四がなくて、五にプレビューです。
UIKitからSwiftUIへ移行した方なら100人中100人が同意していくれると思うのですが、プレビュー機能がとても素晴らしいです。
UIKitを使っていたときは
- UI関連のコードの変更
- ビルド
- シミュレーターで確認する
というステップが必要だったのですがSwiftUIではコードを変更した瞬間にプレビューに反映されます。
Webフロントエンド開発のホットリロードに近いです。
まだプレビューを体験していない方は公式チュートリアルを試すことを強くおすすめします。
SwiftUI導入の壁
TimeTreeでは2020年からSwiftUIを本体アプリに入れていますが、このときはホーム画面に置くウィジェットを開発するために採用しているのみでした。
本格的に取り入れ始めたのは2021年からですが、このときに2つの壁にぶつかりました。
プレビューが遅い|表示されない
本体アプリでSwiftUIのプレビューを実行してみたところ、ビルドから表示までの時間がかなりかかってしまいました。
また、プレビュー時に原因を特定することが困難なエラーが多発し、結果的にアプリをビルドしてシミュレーターで確認するほうが早いという本末転倒の状況になってしまいました。
機能不足
2020年はSwiftUIが登場して1年ほどしか立っておらず、UIKitと比べると機能が足りない状況でした。
Appleが提供している設定アプリのように、「基本的なレイアウト」と「簡単な画面遷移」で構成されているようなアプリはSwiftUIのみでも実現可能でしたが、このとき企画されていたToday機能(現在は提供終了)の実装には不安が残りました。
SwitUIのプレビューを活かすための戦略
これらの問題に対処するために以下の戦略を取ることにしました
- 機能ごとにフレームワーク化し、プレビュー時に必要なビルド時間を減らす
- SwiftUIはコンポーネントの作成のみに使う。複雑なレイアウトと画面遷移はUIKitで行う。
TimeTreeのプロジェクト構成&コードに近いXcodeプロジェクトを含んだリポジトリを用意しました。
以下↑のプロジェクトを参考に1, 2について具体的に説明します。
1. 機能ごとにフレームワーク化し、プレビュー時に必要なビルド時間を減らす
SwiftUI導入前、TimeTreeプロジェクトでは1つの巨大なAppにほぼ全てのUI関連コードがありました。
- UIKitSwiftUISample(App)
- MainViewController.swift
- FeatureA
- FeatureAViewController.swift
- FeatureB
- FeatureBViewController.swift
この構成でFeatreA関連のプレビューを表示する場合、UIKitSwiftUISampleをターゲットにビルドをするため本来プレビューに必要のないコードもコンパイルする必要が出てきてしまいます。
TimeTreeは2015年のリリースを期に膨大なコードベースを抱えており、この本来プレビューに必要のないコードも大量に存在します。これが原因でビルドに時間がかかり結果としてプレビューの表示速度も遅くなってしまいます。
FeatureAの画面をプレビューするのにUIKitSwiftUISample全体をビルドする必要がある
ホーム画面に置くウィジェットを開発したときはこの問題は発生しませんでした。
なぜかというと、ウィジェットは独立したフレームワークで開発する必要がありTimeTree本体のコードを含んでいなかったからです。
これをヒントに機能を別フレームワークとして切り出すことにしました。
- UIKitSwiftUISample(App)
- MainViewController.swift
- FeatureAViewController.swift
- FeatureBViewController.swift
- FeatureA(フレームワーク)
- FeatureAContainer.swift
- FeatureB(フレームワーク)
- FeatureBContainer.swift
FeatureAの画面をプレビューするのにFeatureA関連のコードだけビルドすればいい
この対応のお陰でビルド時間が減少しプレビューを実用的に使うことができるようになりました。
新規機能開発の場合は既存コードのリファクタリングをせずにフレームワークを追加するだけですぐSwiftUIのプレビューが使えることも良かったです。
(プラスでビルド範囲を狭めた副産物としてプレビューのエラーの原因特定が簡単になりました)
2. SwiftUIはコンポーネントの作成のみに使う。複雑なレイアウトと画面遷移はUIKitで行う。
UIKitベースで作られたTimeTreeの既存コードを活かすために、画面遷移と複雑なレイアウトに関してはSwiftUIからUIKit側に委譲することにしました。
UIKitのモダンCollectionViewのレイアウトを使って、セルの中身だけSwiftUIを使う方法に関しては以前 @gonsee が記事を書いてくれているのでここでは割愛します。
UIKit側に画面遷移を委譲するために、SwiftUIからUIKitにCombineを使って画面遷移のイベントを伝える方法を採用しています。
この方法を使うと既存のUIKitベースのアプリで無理なく画面遷移を行うことができます。
具体的には以下のように実現しています。
- 画面遷移イベントと遷移に必要なデータをenumで定義
- ViewModelに画面遷移イベントを発火するためのPassthroughSubjectを定義
- UIKit側(UIViewController)で2を監視
- SwiftUI側で画面遷移イベントを送信
1. 画面遷移イベントと遷移に必要なデータをenumで定義
SwiftUIのViewクラス内に Transition
というenumを定義しています。Associated Valueに画面遷移に必要なデータを定義しています。
2. ViewModelに画面遷移イベントを発火するためのPassthroughSubjectを定義
1の Transition
をUIKit側に伝えるためにViewModelにPassthroughSubjectプロパティを持たせています。画面遷移が必要になったらこのPassthroughSubjectのsendメソッドを叩きます。
3. UIKit側(UIViewController)で2を監視
UIViewController初期化時に3のPassthroughSubjectをsinkしてイベントを監視します。
こうすることでUIKit側の画面遷移のメソッドが使えます。
4. SwiftUI側で画面遷移イベントを送信
SwiftUI側で画面遷移が必要になったタイミングでPassthroughSubjectのsendメソッドを叩きます。sendする Transition
の種類によって遷移先を変更できます。
まとめ
機能ごとにフレームワークを分け、複雑な画面遷移とレイアウトをUIKit側に以上することで、UIKitベースの大規模アプリでもSwiftUIのプレビューの恩恵に預かれるようになりました。
2021以降、TimeTreeでは以上の方法を使ってUI開発の主戦場をSwiftUIに乗り換えています。
が、まだまだ暫定という感じで、、、より良い設計を模索中でもあります🕵️
(一緒に模索してくれる方、TimeTreeで一緒に働きませんか?)
お世話になった記事
機能ごとにフレームワークに分割する際にクックパッドさんの「コード生成を用いたiOSアプリマルチモジュール化のための依存解決」の記事を参考にさせてもらいました🙏
TimeTreeの採用情報
TimeTreeのミッションに向かって一緒に挑戦してくれる仲間を探しています。TimeTreeで働くことに興味がある方はぜひ、Company Deck(会社紹介資料)や採用ページをご覧ください!
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion