【iOS】UIKit+CombineからSwiftUIを使った独自アーキテクチャへ
この記事は KINTOテクノロジーズアドベントカレンダー2024 の8日目の記事です🎅🎄
はじめに
こんにちは。モバイルアプリ開発グループの中本です。
普段は、大阪にいながら東京のメンバーと協力して、KINTO UnlimitedアプリのiOS開発をしています。
本記事では、KINTO Unlimitedアプリ(iOS)のアーキテクチャ改善の過程について詳しく説明します。
このアプリのアーキテクチャは、1st → 2nd → 3rdと段階的に進化し、最終的には独自のアーキテクチャへと移行しました。
それぞれの段階における設計や課題について、以下でお話しします。
1st Generation Architecture
- VIPERアーキテクチャを採用
- すべての画面をUIKit + xib/storyboardで実装
- Combineを使用してViewを更新
- ファーストリリースに向けて納期が短かったこともあり、社内で実績のあったアーキテクチャを採用
1stの設計
-
ViewController
- ViewModelにイベントを通知
- ViewModelからのイベントに基づいてアウトプットを購読
- 購読結果に応じてViewを更新し、Routerを呼び出して画面遷移を実施
-
ViewModel
- Combineを使用してリアクティブに状態を変化
- イベントPublisherを変換し、Viewの状態をPublisher経由でアウトプット
- Combineを使用してリアクティブに状態を変化
-
Interactor
- APIや内部DBにリクエストを実施
-
Router
- 他の画面への遷移処理を実施
-
UIView
- コード/xib/storyboardを使用してレイアウト
1stの課題
- UIKitを使用したレイアウトは開発コストが高く、特にxib/storyboardを使用した場合は変更が容易ではない
→ SwiftUIへ移行したい!
2nd Generation Architecture
- UIKitからSwiftUIへ移行
- UIKitによるレイアウトをSwiftUIに置き換え、開発効率を改善
- UIHostingControllerを使用してSwiftUIのViewをViewControllerに注入
- 画面遷移は従来通りUIKitで実施
- 当時、SwiftUIの画面遷移APIは不安定だったためUIKitのまま
- SwiftUIへの移行に専念する
- 一度にたくさん変更すると、機能仕様のデグレが懸念されるため
- UIKitによるレイアウトをSwiftUIに置き換え、開発効率を改善
2ndの設計
-
ViewController
- HostingControllerInjectableプロトコルを実装し、SwiftUI Viewを追加
- ViewModelのアウトプットを購読し、ScreenModel(ObservableObject)に反映
- ViewModelのアウトプットやScreenModelのPublisherを購読し、Routerを用いて画面遷移を実施
-
ScreenModel
- Viewの状態を保持するObservableObject
-
ViewModel / Interactor / Router
- 1st Generationと同様の機能
2ndの課題
-
状態管理がViewModelとScreenModelの両方で行われるため、ロジックが分散し、開発・保守コストが増加
-
1stからの課題
- Combineによるリアクティブな状態変化の実装は保守性に懸念があり、コード量が多く可読性に難がある
- 1画面につき1つのViewModelであるため、機能の多い画面ではViewModelが巨大化
→ CombineやViewModelから脱却したい!
3rd Generation Architecture
- Combineを用いたViewModelから状態を集中管理するViewStoreを中心としたアーキテクチャへ移行
- イベントの結果をAnyPublisherを経ずに直接ObservableObjectに反映できる仕組みを実現
- Combineを使用せず、async/awaitを用いてリアクティブな状態変更を実現
- 状態管理ロジックを機能ごとに分割可能
3rdの設計
-
ViewStore
-
State
- Viewの状態を保持するObservableObjectで、SwiftUIのViewで使用
-
Action
- 従来のViewModelのtransformメソッドにおけるINPUTに相当する機能を提供するenum
-
ActionHandler
- Actionを引数にとり、Stateを更新するハンドラー
- async/awaitを使用して実装
-
-
ViewController
- routerSubjectを購読し、Routerを用いて画面遷移を実施
-
Interactor / Router
- 2nd Generationと同様
ActionHandlerの分割
機能の多い画面では、ActionHandlerとStateを分割することで、コードの可読性や保守性を向上させることができる
- StateのactionPublisherを他のStateにバインドすることで、あるViewから他のViewにアクションを送ることが可能
おわりに
今回の取り組みは、機能開発と並行して1年以上かけて進めてきました。
現在では、ほぼすべてのソースコードが3rd Generation Architectureに置き換わっています。
その結果、コードの可読性や保守性が向上し、今後の開発が非常にやりやすくなったと感じています。
引き続き、さらなる改善を重ねていければと思います!
関連記事 | Related Posts
We are hiring!
【iOSエンジニア】モバイルアプリ開発G/大阪
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。
【iOSエンジニア】モバイルアプリ開発G/東京
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。