こんにちは。 iOS エンジニアの千吉良(ちぎら)です。
今回は iOS アプリの設計をサポートするフレームワークとして ReactorKit を導入した話をします。
動機
Mirrativ の iOS アプリは元々「1ViewControllerあたり1Storyboard」という構成で作られていました。
ViewController内の実装に関しては特に実装方針を定めていませんでしたが、多くの方が実装に関わっていくにつれて、実装方針が決まっていないことは、新しく開発に参加される方の戸惑いや、特殊なケースに特化した独自の設計が導入されていく危険性につながるのではないかという懸念が出てきました。メインの課題はそれらの懸念を解消することなのですが、設計を決めるにあたって、いくつかの前提がありました。
- 機能開発を並行で進めたいので、部分的に適用できるものにしたい
- モデル層は型安全な形式に移行しているので、モデル層の構成に密に依存しないようにしたい
- AndroidにFluxを導入しそうな機運があり、思想は近いものにしたい
Clean Architecture や MVVM, VIPER など様々なアーキテクチャがありますが、上記の前提と懸念の解消のために、ReactorKit を設計をサポートするフレームワークとして選びました。
ReactorKit とは
ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit and describes how to build an application using ReactorKit.
...
ReactorKit is a combination of Flux and Reactive Programming.
上記の README から引用した文章から分かるように、ReactorKit はリアクティブかつ単一方向のデータフローの為のフレームワークです。Design Goal として、以下の3つが挙げられています。
- Testability
- Start Small
- Less Typing
Small Start, Less Typing は、機能開発と並行で設計を導入していく上で魅力的な要素でした。特に既存のプロダクトに新たに設計を導入する場合は、適度な記述量で精神的な負担が少ないことも、無理なく開発を進めていく上で大事な要素だと思います。
以下に ReactorKit のコンセプトの概要図を引用します。
ReactorKit は大きく View
と、対となる Reactor
という層から構成されます。
図に書かれている View
は、プログラム上では UIViewController
や UITableViewCell
に相当する単位です。既存のプログラムが「1ViewControllerあたり1Storyboard」という粒度で構成されている為、その粒度も変更する必要はなく、新しく作る ViewController から ReactorKit に準拠していけばいいという非常にお手軽な状況を作ることができました。
Action
と State
は、View
と Reactor
のコミュニケーションの為に使用される要素ですが、それらは Reactor
ごとに定義することになるので、既存のモデル層の作りにそれほど依存せずに実装していくことができます。
つまり、上に挙げた
- 機能開発を並行で進めたいので、部分的に適用できるものにしたい
- モデル層は型安全な形式に移行しているので、モデル層の構成に密に依存しないようにしたい
- AndroidにFluxを導入しそうな機運があり、思想は近いものにしたい
を全て満たしてくれます。
ReactorKit と Flux
ReactorKit には Reactor という UI とは独立した層が存在しています。 View
が Action
を発行し、 Reactor
が内部で処理をして State
を変化させ、 State
の変化を View
が受け取るという流れをフレームワークでサポートすることで、単一方向のデータフローを実現しています。
Action
や Mutation
は enum で定義され、簡素で扱いやすい形式で表現ができます。下記のコードは README にあるサンプルコードから持ってきたものですが、シンプルな定義のReactorでうまく単一方向のデータフローがサポートされていることがわかると思います。
class ProfileViewReactor: Reactor { // Viewから渡されるActionはenumで定義する. Associated Valueをパラメータとして利用している. enum Action { case refreshFollowingStatus(Int) case follow(Int) } // Stateを変化させるMutationもenumで定義する. enum Mutation { case setFollowing(Bool) } // View側でstateの変化に応じてUIを更新している. struct State { var isFollowing: Bool = false } let initialState: State = State() // Action → Mutation func mutate(action: Action) -> Observable<Mutation> { switch action { case let .refreshFollowingStatus(userID): // receive an action return UserAPI.isFollowing(userID) // create an API stream .map { (isFollowing: Bool) -> Mutation in return Mutation.setFollowing(isFollowing) // convert to Mutation stream } ... } // Mutation → State func reduce(state: State, mutation: Mutation) -> State { var state = state // create a copy of the old state switch mutation { case let .setFollowing(isFollowing): state.isFollowing = isFollowing // manipulate the state, creating a new state return state // return the new state } } }
チームへの共有と浸透
新しい設計をいきなり導入することは難しいので、3ヶ月の猶予を持って「新規で作成する画面はすべて新しい設計に準拠している」ことを目指しました。僕自身もReactorKitを実務で使用するのは初めてだったので、まずは既存のプロジェクトに導入をしてリファレンス実装を作るところからはじめました。浸透までは、大まかには以下のような流れで行いました。
- 1ヶ月目
- 簡単な画面をReactorKitの新設計に書き換え、実装する際の参考にしてもらう為のリファレンス実装とする
- 2ヶ月目
- リファレンス実装のコードを実例として添えながら、新設計の思想やルールなどを簡単にまとめたドキュメントを作成する
- 3ヶ月目
- 啓蒙
結果としては、3ヶ月経過時点で「新規で作成する画面はすべて新しい設計に準拠している」状態になり、新しく関わっていただく方にも迷いなく設計方針を伝えられる環境ができました。
また、XcodeのテンプレートにReactorKitの為のテンプレートを追加して、新規クラス作成時の負担を軽減しています。
頭を使わない作業をテンプレートにすることで少しだけ楽ができるようになりました。
まとめ
今回は、MirrativのiOSアプリにReactorKitを導入したという話をしました。これまではViewControllerにすべてのコードが書かれていて非常に自由度が大きく、人によって実装に差があり、実装方針もその都度考えていましたが、方針が定まったことで実装やコミュニケーションのコストを減らすことができました。今後もより楽が出るように改善を進めていきたいと思います。
We are hiring!
Mirrativ では一緒に開発してくれるエンジニアを募集しています!
体験入社や副業も大歓迎なのでお気軽にどうぞ!
2/20 にCTOが登壇するミートアップも開催するそうです!お気軽に遊びにきてください!