iOSエンジニアの金子です。
NewsPicks iOSアプリでは2021年7月ごろからThe Composable Architecture(TCA)を採用したアーキテクチャへの移行を進めています。
メインのニュースフィード画面もTCA+SwiftUI/UIKitで全面的に書き換え、TCAを使った実装もだいぶこなれてきたなと思っていた矢先、TCAで大きなアップデートがありました。
このReducerProtocolへのマイグレーション作業を1月から開始し、1ヶ月ちょっとかけてようやく全てのReducerをReducerProtocol化することができました。
本記事では、NewsPicksで取り組んだReducerProtocolへのマイグレーション作業についてご紹介します。
どのようにマイグレーションを進めたか
公式ドキュメントに素晴らしいマイグレーションガイドが用意されていて、ほぼこれ通りにやれば問題ありません。
一部Reducerを独自拡張して実装していた部分があったので、そこは一旦標準的な書き方に直した後にマイグレーションをしましたが、NewsPicksではライブラリを素直に使ってきたので、マイグレーションで詰まることは全くありませんでした。
「マイグレーションガイド通りやればできます!」という実績を作って発表できたのは良いことですが、さすがにそれだけだと記事として物足りないので、チームでどのように分担して進めたかという部分をご紹介します。
タスクの洗い出し
最初は僕の方でいくつかのCore(State、Action、Environment、Reducerをまとめた名前空間)をピックアップし、マイグレーションをしてみました。 TCA導入初期に実装した部分と最近実装した部分とでは実装の仕方が若干異なっていたので、それぞれのパターンごとにどのように書き換えをすれば良いかのリファレンスとなるPRを用意しました。
次に、Github Discussionsにマイグレーション作業の進め方ガイドを作成し、チームメンバーに説明をしました。
その後、プロジェクトから全てのCoreを洗い出し、1個ずつのマイグレーションタスクとしてタスク管理ツールに登録しました。 最初に洗い出したときはCoreは140個ほどあり、マイグレーションを進めていく中で20個ほど見えていなかったものが出てきたので、最終的に160個ほどのマイグレーションタスクを実施したことになります。
タスクをチームで分担
後はチームで分担してひたすらマイグレーションしていくのですが、コミットをシンプルにしたりコンフリクトを避けるために工夫した点があります。
公式のマイグレーションガイドにも記載がありますが、他のReducerを組み込んでいない枝葉のCoreからマイグレーションしていくのが良いと思います。 枝葉からはじめて、ルートに向かっていくイメージです。
このとき、同じ親につながるCoreを分担するとコンフリクトするので、下図でいうとNewsFeedXxx担当、FollowFeedXxx担当という感じで分担するのが良さそうです。 ある程度全体の構造がわかっている人がファシリテーションしていくのがスムーズですね。
また、Git/Githubに関する細かいTipsも紹介しておきます。
僕らの場合、Coreを実装したファイルにはXxxCore.swiftという名前をつけていて、マイグレーションした場合はXxxReducer.swiftという名前に変更するようにしていました。 このとき、ファイル名変更と実装の修正を同じコミットで行ってしまうと、場合によってはファイル削除・追加という差分として検出されてしまい、コードレビューがしづらくなります。
そこで、ファイル名変更とコード修正のコミットを分けます。 こうすることで期待する差分が見れ、コードレビューがしやすくなります。
マイグレーションによる効果
ReducerProtocolにしたことで、Reducerの書き方で悩む必要がなくなったのは嬉しいポイントです。
以前はCoreでまとめる方針にしていてこれはこれで機能してましたが、プロトコルで書き方が強制されるので、より統一感が出て可読性が高まってます。
また、以前はViewStateや共通ロジックの実装方法で悩んでいましたが、公式ドキュメントが充実してこれらの実装についても言及があり、マイグレーションのタイミングで全てのReducerの実装を公式推奨の書き方に統一しました。
Point-Freeの動画ではReducer内のコード補完が改善されると言っていましたが、これについてはいまいちって感じです。
以下のような実装の場合、state.
のところでCtrl+SpaceしたらStateのプロパティのみを候補として出してほしいところですが、スコープ外のプロパティの候補もたくさん出てきてしまいます。
struct SomeReducer: ReducerProtocol { struct State: Equatable { let id: Int let name: String } ... var body: some ReducerProtocolOf<Self> { Reduce { state, action in switch action { case .buttonTapped: state. // ここではidとnameを候補として出してほしいが、スコープ外のプロパティの候補が大量に表示される... ... }
枝葉のReducerだとStateのプロパティのみが候補に出てきてうまく機能してるようだったので、複数のReducerを結合してるところで起きる問題なのでしょうか...?
コード補完は生産性に直結する要素なので、今後原因を追って行きたいと思っています。
Dependencies対応に着手
既存のCoreをReducerProtocol化するという対応は完了しましたが、いわゆる依存のバケツリレーという大きな課題(詳しくは前述のブログ記事の #依存関係更新が面倒 のセクションを参照してください)は残っていて、最近ここの対応に着手し始めたところです。
これまでAPIリクエストやDBアクセスを担うRepositoryをProtocolスタイルで実装してきましたが、Dependencies対応と合わせてPoint-Freeが推奨するstructスタイルの実装への置き換えを進めています。
Dependencies対応についてはある程度進んだらまたブログに書きますので、どうぞお楽しみに!
※Dependenciesおよびstructスタイルでの依存の実装については以下の記事にまとめていますのでご参照ください
まとめ
NewsPicks iOSアプリで実施したReducerProtocolへのマイグレーション作業についてご紹介しました。
マイグレーション作業自体はひたすら単純作業という感じなので、最初に進め方さえ決めてしまえばあとはやるだけという感じです。 リファレンスとなるPRをいくつか用意しておけば、TCAの初学者でも特に問題なく進めることができると思います。
本記事がこれからマイグレーションをしようとしている方に少しでも参考になれば幸いです。