SwiftUI+TCAに挑戦!NewsPicks iOSアプリのリアーキテクチャについて

NewsPicks iOSエンジニアの金子です。


最近あちこちでリアーキテクチャについての話をしているのですが、割とふわっとした内容に留まってしまっていたので、もう少し具体的にどういうことをしているかをお伝えするために記事を書くことにしました。

本記事では、リアーキテクチャの背景、リアーキテクチャで目指していること、リアーキテクチャで採り入れている技術について触れたいと思います。

tech.uzabase.com

tech.uzabase.com


なお、Androidアプリのリアーキテクチャの話は以下の記事で詳しく紹介しているので、是非合わせてご覧ください。

tech.uzabase.com

リアーキテクチャの背景

NewsPicksのiOSアプリは最初のリリースから8年以上が経過しています。

その間、ビジネスはもの凄いスピードで成長し、その時々のニーズに合わせてUIは何度もリニューアルされてきました。

一方で、アプリのコードは最初のリリースからその構造を大きく変えることはなく、修正に次ぐ修正により、保守性がかなり低い状態になっていました。


具体的には以下のような課題がありました(細かいこと言い出すと色々あるけどまとめるとこんな感じ)。

  • いわゆるFatViewController、FatPresenter状態で、ロジックを追うのが非常に困難
  • シングルトンクラスが多用され、そこに状態を持たせているため、状態がどこでどう変わるのか分かりづらい
  • API通信、ドメイン・プレゼンテーションロジックが混在していてユニットテストが書けない(ユニットテストは一切無かった)
  • 上記のような状態なので、想定したスケジュール通りに開発が進まなくて開発者が疲弊したり、考慮不足でバグが発生するといったことも頻繁に起きる


なぜこうした課題が生まれてしまうのかと言うと、シンプルに実装ルールが無かったからです。

実装ルールが無いから、開発者はそれぞれ好きなように実装するし、レビュアーもルールが無いから指摘しようもない。


僕は昨年からiOSアプリの開発を本格的にするようになったのですが、いつ壊れるかもわからないという不安を抱えながら実装するという経験をして、今のアーキテクチャの限界を感じました。

同様にAndroidアプリのアーキテクチャにも課題意識を持っていた石井さんがモバイルアプリチームにジョインしたことを契機に、iOSアプリのリアーキテクチャを行うことにしました。

リアーキテクチャで目指しているもの

いち開発者として前述のような状態で実装をしていくのは単純に楽しくない、というのが個人的にはリアーキテクチャのモチベーションになっていますが、組織としてのモチベーションは以下の2つです。

  1. 開発スピードを上げること
  2. 事業チームとモバイルアプリ開発者が蜜に連携できる体制を作ること


ルールが無く無秩序なコードは可読性が低いため、既存仕様を把握するのに非常に時間がかかります。場合によっては実装した人に聞いたり、アプリを動かしてデバッグしないとわからないこともあります。このような状態だと、新規にジョインしたメンバーがなかなかアウトプットを出せないので、人を増やしてもなかなか開発スピードが上がらないという問題が起こります。

NewsPicksは事業領域が非常に広く、開発要望がひっきりなしにやってくるのですが、現状ではビジネススピードに開発スピードが追いついていない状態になってしまっています。

リアーキテクチャで可読性の高いコードにすることで、新規にジョインしたメンバーでも少しのサポートで速く戦力になれるようにすることを目指しています。


また、リアーキテクチャをすることで組織構造を柔軟に変更できるようにしたいと思っています。

現在のプロダクトチームはバックエンドエンジニアのメンバーが事業ごとにチームを組成していて、モバイルアプリエンジニアは1つの組織に集約されています。そして、我々モバイルアプリチームは全ての事業チームからの要望を一手に引き受けるという組織構造になっています。

今の組織構造ではモバイルアプリエンジニア同士の連携がしやすいため、ノウハウの共有やレビューがしやすいというメリットがあります。一方で、事業チームからの要望を受けて開発をするという受託体制になるため、モバイルアプリエンジニアが課題解決的な思考になりにくいというデメリットがあります。

その思考はコードにも表れます。仕様書に書いてある通りに実装すればたしかにそのロジックで良いけど、既存ロジックとの整合性が取れてなかったり、そもそもその仕様でユーザの課題が解決できるのか、レビューをしてて疑問に思うことがあります。

リアーキテクチャによって実装ルールが明確になり、iOSエンジニアがバラバラになっても保守性の高さを維持していける土台ができていれば、各事業チームにiOSエンジニアを配置するという体制を取ることもできるようになります。

ビジネスの状況によって最適な組織体制は変わると思うので、組織構造の変化に柔軟に対応できるアーキテクチャを作ることが目標です。

リアーキテクチャで採り入れている技術

実装ルールを明確にするといっても、特定のアーキテクチャを採用するだけでは不十分です。

採用したアーキテクチャ、ルールを長期に渡って維持していくためには、それらを強制するための仕組みが必要です。

現在採り入れているものとしては、以下があります。

  • マルチモジュール化
  • The Composable Architecture(TCA)
  • SwiftLint

マルチモジュール化

リアーキテクチャをするにあたり、ユニットテストが書きやすい構造にすることは大事なポイントだと思っていました。

ユニットテストの主な目的はコード変更時のデグレ検知かと思いますが、個人的にはユニットテストを書くことを前提に実装すると良い設計になることのメリットが大きいと思っています。

依存が多く、多くのロジックを持つクラスやメソッドのユニットテストは非常に書きづらいです。これは悪い設計の兆候です。

ユニットテストを書くことを意識しながらプロダクションコードを実装すると、自然と良い設計のコードになるはずです。


既存のコードではPresentationレイヤー(ViewControllerやPresenter)で直接API通信を行うクラスを参照しているため、ユニットテストが書きづらい構造になっていました。

新しいアーキテクチャではまずこの部分を改善することを念頭に起き、レイヤードアーキテクチャを参考にした構造にしています。

具体的には、コードを以下の4つのレイヤーに分け、さらにそれぞれをフレームワーク化して依存関係を強制しています。

  • Domain
  • Infrastructure
  • Presentation
  • App

※現状AppとPresentationは同一モジュールになってしまっているので、理想の形にはできていません
※汎用Extensionだけをまとめたモジュール、Resourceだけをまとめたモジュールなども作成しているのでモジュールが4つというわけではないです


依存の方向は以下のようになっています。


API通信を行うRepositoryの実装をInfrastructureレイヤーに置き、Repositoryのインターフェース(プロトコル)をDomainレイヤーに置きます。

PresentationレイヤーはInfrastructureレイヤーを直接参照できないため、DomainレイヤーのRepositoryインターフェースに依存させます。

DIはAppにて行います。

これにより、Presentationレイヤーのユニットテストを書くときはRepositoryをモック化することができるので、テストが書きやすくなります。


さらに、PresentationレイヤーからDomainレイヤーを切り出すことで、ドメインロジックがあちこちに散らばることを防いだり、Domainレイヤーはどこにも依存しないのでロジックをシンプルに保つことができるなどのメリットも得られています。

今後は汎用的なUIコンポーネントをまとめたモジュールを作成し、UIコンポーネントのカタログミニアプリを作るなども検討しています。


こうしたレイヤー分けはルールを決めて運用していけばできることではありますが、開発者が増えていくとレビューで間違いを指摘するのも限界が来るので、そもそも開発者が間違えられない仕組みを整えることが重要です。

レイヤーをフレームワーク化して依存関係を強制することで、長期に渡って適切な構造を保つことができるようになります。

Feature単位でのマルチモジュール化も検討中

NewsPicksは事業領域が広く、アプリもそれに対応するために機能も画面も非常に多くなっています。

前述したようにiOSエンジニアが各事業チームに所属する組織構造になった場合は、クックパッドさんがやっているようなFeature単位でのマルチモジュール構成の方が事業ごとに並行開発しやすいのかなと想像しています。

ただ、Feature単位でのマルチモジュール化をするためには、Feature間での直接依存を避けるための依存関係解決の仕組みを用意する必要があり、対応にはそれなりに時間がかかりそうです。

レイヤー単位でのマルチモジュール化で保守性を維持しつつ、Feature単位でのマルチモジュール化も並行して検討を進めていこうと思っています。

The Composable Architecture(TCA)

本格的にリアーキテクチャを開始する以前から、新しいアーキテクチャではReduxを採用したいと思っていました。

iOSエンジニアのみなさんはご存知の通り、iOSアプリでは複雑で多様な状態変化を管理必要があります。

加えて、NewsPicksではデータドリブンな開発プロセスを取っており、アプリから多種多様なログを送信しています。画面の状態に応じてログに様々なパラメタを付与する必要があり、ログのためだけにクラス間でパラメタの受け渡しをしていたりして、コードの複雑さの原因の一つになっています。

個人的にReSwiftを使ってReduxを導入した経験があり、Reduxがこうした複雑さを一定解消してくれることは知っていたので、Reduxの採用を前向きに考えていました。


また、新しいアーキテクチャではSwiftUIを積極的に使っていきたいとも思っていました。当時は(今もですが...)SwiftUIのコンポーネントが不足していて満足のいくアプリが作れるのか不安がありましたが、今後SwiftUIがiOSアプリ開発では主流になっていくと思っていたので、思い切って本格採用することにしました。

SwiftUIに最適なアーキテクチャは何かと色々調べていたところTCAを見つけ、サンプル実装を行ってこれは良いと思ったので採用しました。


TCAを具体的にどのように使っているかについては改めて記事を書こうと思っていますが、リアーキテクチャの観点で言うと以下のメリットがあると感じています。

  • コードの書き方をある程度統一できる
  • DIの仕組みが用意されている
  • ユニットテストのサポートがしっかりしている
  • UIKitでも使える


TCAの設計原則に従うと、コードの書き方はある程度統一されます。ViewでActionを送信し、ReducerがActionを受け取ってStateを変更し、その変更がリアクティブにViewに反映されるという流れが基本になります。

もちろん、ビューに直接Stateを持たせたり(ものによってはその方が適切なケースもあるような気がしている)、ビューでAPI通信する処理を書くこともできてしまうので逸脱した書き方を排除できるわけではないのですが、TCAという設計指針があることで例外は少なくなるでしょう。

実際、TCAを導入したことでコードの可読性は格段に良くなりました。既存動作を追うときはActionを見ればどのような機能があるか把握できるし、Stateがどのように変更されるかはReducerを見ればわかるので、コードの修正を行うときの初動の速さが以前と比べて全然速くなりました。


テスタブルなアーキテクチャにする上でDIの仕組みをどうするかは課題となりますが、TCAにはその仕組みが備わっています。

以下はとあるコンポーネントのEnvironmentの一部ですが、依存先をRepositoryインターフェースとしています。プロダクションではDIによってRepositoryの実装インスタンスが注入されますが、ユニットテスト時はRepositoryインターフェースをモック化することでReducerのテストが書きやすくなります。

struct Environment {
    let newsFeedRepository: NewsFeedRepository
    let analyticsLogRepository: AnalyticsLogRepository
}


ユニットテストのサポートがしっかりと用意されているのもポイントです。

TestStoreというテスト用のStoreがあるのですが、これを使うとActionを送信したあとにStateがどういう状態になっているべきかをテストすることができます。そのActionが別のActionを呼び出す場合は両方のActionの結果を検証する必要があるので、テスト漏れを防いでくれます。

// onAppearアクションを送信
store.send(.onAppear) {
    // onAppearアクションが処理された後のStateのあるべき状態
    $0.shouldShowLoadingIndicator = true
}

// onAppearアクションによって別のアクションが発火
store.receive(.fetchItemsResponse(.success(entity))) {
    // fetchItemsResponseアクションが処理された後のStateのあるべき状態
    $0.items = expectedItems
    $0.shouldShowLoadingIndicator = false
}


UIKitでも使えるというのもポイントです。

TCAのコンポーネント(State、Action、Environment、Reducerで構成される機能の単位)はViewがUIKitなのかSwiftUIなのかについては関知しないので、フィードはUIKit、セルはSwiftUIといった構成を取ることも可能です。既存のUIKitのViewにコンポーネントを入れることもできます。

NewsPicksでは最近メインフィードを新しいアーキテクチャでリニューアルしたのですが、最初SwiftUIで実装したところスクロールのパフォーマンスが悪かったり保守性の悪いコードになってしまったりしたので、UIKitで実装し直しました。作り直す過程でコンポーネントは多くがそのまま利用できたので、ビューの詳細とコンポーネントとが疎結合になっていることがわかります。


メリットをいくつか挙げましたが、やはりデメリットも感じています。

TCAは公式で素晴らしいサンプルが用意されていて、isowordsという大規模なリファレンス実装もあるので、最初のキャッチアップは結構しやすいです。しかし、使いこなしていくにはそれなりに時間と労力が必要で、かつアップデートも速い(最近だと大きなアップデートとしてSwift Concurrency対応もありました)ので継続的に追いかけていく必要があります。TCAを導入してそれなりに時間が経っていますが、まだまだ試行錯誤しているところも多いので、iOSエンジニアを分散配置できるほどかっちりとしたアーキテクチャにはまだ仕上がっていません。

また、何か問題にぶつかったときに、それがSwiftUIの問題なのかTCAの問題なのか切り分けるのに苦労します。SwiftUIでフィードを実装していてスクロールのパフォーマンスが悪くなってしまったときに、SwiftUIのビューの組み方に原因があると見て調査していたのですが、スクロール時にActionを大量送信していたのが原因だと後でわかったことがあります。

SwiftUIとTCAの両方にしっかりと習熟していないと苦労することも多いので、TCAを導入する際は多少慎重になってもいいかもしれません。

SwiftLint

コードの書き方に強制力を持たせる上でSwiftLintは強力な武器になります。ビルド時にSwiftLintが実行されるようにするだけでなく、Dangerと組み合わせてプルリクエストにWaringやErrorを通知できるようにして、ルールに従っていないコードがmasterにマージされることを防いでいます。

ただ、ルールのカスタマイズが十分にできていなかったり、新しいアーキテクチャに合わせてカスタムルールも作りたいと思っていて、まだまだ改善の余地があります。

最後に

NewsPicks iOSアプリのリアーキテクチャの背景、リアーキテクチャで目指していること、リアーキテクチャで採り入れている技術についてご紹介しました。

リアーキテクチャを本格的に開始して1年ほど経過していますが、目指しているアーキテクチャになるにはやることがまだまだたくさんあります。

現在NewsPicksではiOSエンジニアを絶賛募集中です。SwiftUI、TCA、リアーキテクチャといったキーワードに興味がある方は、是非是非お声がけください!

Page top