CA.swiftは、サイバーエージェントのABEMAやAmeba、AWA、tappleなどを担当しているiOSエンジニアによるiOSエンジニアのための勉強会です。 それぞれのチームで使用している技術や開発体制など、開発の中でのノウハウを惜しみなく発信していきます。 

第22回のテーマは 「Swiftの進化を活かした技術基盤への挑戦」 です。サイバーエージェントでは、Swift ConcurrencyやSwiftUI、TCAなど次世代技術を活用しながら、いかにして現場の技術基盤を進化させてきたのか。そのプロセスで生まれた試行錯誤や、技術選定のポイント、実例に基づく工夫の数々を共有する予定です。

本記事は、2025年01月08(水)に開催した「CA.swift #22」において発表された「SwiftUI導入から1年、SwiftUI導入とVueFluxライクな状態管理」に対して、社内の生成AI議事録ツール「コエログ」を活用して書き起こし、登壇者本人が監修役として加筆修正しました。


吉田 周平 (  FANTECH本部 > CL事業)

GitHub : ShuheiYoshidaJP
X : TyrionJP

2024年4月に新卒入社し、LDHの配信アプリCLの開発を行っています。


SwiftUI導入から1年。SwiftUI導入とVueFlux-ライクな状態管理と題しまして、FanTech本部CL事業部吉田が発表させていただきます。

吉田周平と申します。社内ではしゅうしゅうと呼ばれることが多いです。2024年に新卒入社し、現在はFanTech本部のCL事業部で開発を担当しています。気づけば入社してもうすぐで1年が経ち、時間の流れがとても早く感じています。

次に私たちが開発しているCLについて紹介させていただきます。

CLは、LDHのオリジナル番組や生配信を楽しめる動画配信サービスです。このアプリでは、アーティスト自身が配信するLIVE CAST機能やオリジナル番組の視聴など、様々なコンテンツを楽しむことができます。さらにCLは動画配信だけでなく、推しのアーティストの画像を集める機能や、アーティストやユーザーが文字や写真を投稿できるコミュニティ機能なども備えています。アーティストとファンがより繋がりやすいサービス、ユーザーがいつでも好きなときに楽しめるサービスを目指して、開発を続けています。

これから、なぜSwiftUIを導入することにしたのか、その背景について説明させていただきます。

SwiftUIを採用するにあたって、導入の理由と既存のアーキテクチャの整合性という2つの大きなポイントについて紹介します。まずSwiftUIを導入した理由についてお話しします。

1つ目は、新規でジョインするエンジニアがSwiftUI経験者である可能性が高いという点です。近年のiOS開発では、UIKitではなくSwiftUIをメインで学んできたエンジニアが増えています。そのため、新しくジョインするメンバーがスムーズに開発できる環境を整えたいという意図がありました。

私自身もSwiftUIの方が経験があり、SwiftUIを使った実装はスムーズに行えた記憶があります。

2つ目は、コードの可読性とメンテナンスの向上です。SwiftUIは宣言的なUIを採用しているため、UIの変更が状態変化に紐づく形で自由に記述できます。これによりUIKitのようにイベントのたびにUIを手動で更新するコードを書く必要がなくなり、コードの見通しが良くなるメリットがあります。

3つ目は、Storyboardの差分確認の課題です。多くの方々に共感いただけると思いますが、Storyboardを使用するとコードレビュー時にUIの変更が視覚的なファイルの変更として扱われるため、差分の確認が難しい状況がありました。SwiftUIではコードベースでUIを管理できるため、この問題を解決し、レビュアーの負担を軽減できると考えました。

Model: 次に既存のアーキテクチャとの整合性について説明します。

既存のアーキテクチャと考え方が大きく乖離しないように今回は特に配慮しました。

既存の開発では、VueFluxというライブラリを使用し、単一方向のデータフローを採用していました。SwiftUIへの移行後も、この考え方をできる限り踏襲し、データフローが大きく変わらない形で移行できるようにする工夫が必要でした。SwiftUIは、StateObjectやObservedObjectを使うことで、データの変更に応じてUIを自動更新できる仕組みがあります。

これを活用することによって従来のAction Mutation State UI 更新の流れを大きく変えずに移行できるようにしました。

二つ目は既存のエンジニアが違和感なく移行できる形にすることです。アーキテクチャの設計を大きく変えてしまうと、移行に伴う学習コストが増えてしまいます。

三つ目はアーキテクチャの統一性を確保することです。開発チームで統一した設計を採用することで、コードの書き方に一貫性を持たせチーム全体の開発効率が下がらないように配慮しました。

ここからは Vue Flux とは何かについて簡単に紹介させていただきます。

VueFlux は Flux アーキテクチャや Redux、Vue.js の状態管理ライブラリ、Vuex といった単方向のデータフローの考え方を Swift に取り入れたライブラリです。以前、CyberAgent にいたエンジニアの青山さんが開発したライブラリになります。Store、Action、Mutation という Flux の基本概念をそのまま使えるため、アプリの状態を一元管理して変更を予測しやすい利点があります。

VueFlux が採用している Flux 系のアーキテクチャの流れについて説明します。まず Action でユーザーの操作や非同期イベントを表し、それを Mutation に渡して状態を変更します。

状態は State で集中管理されていて、VueFlux の場合は Computed がこの State から派生される値を計算します。View は最終的に Computed や State を購読して UI を更新する形になります。単一方向データフローという特徴があり、常に Action、Mutation、State、View の方向にデータが流れるため、バグ調査や状態の把握がしやすいメリットがあります。

CLにおける Vueflux の活用方法について、具体的なコード例を交えながらご紹介します。

CLでは一つの画面につき主に ViewController、Adapter、State、Mutation、そして DataModel の計5つの要素を定義します。

はじめに Adapter と ViewController の役割を見ていきましょう。

Adapter は API や画面遷移といったメソッドを提供する役割を担います。ユーザーからの操作があると ViewController 内の Adapter のメソッドが呼び出されます。今回の例では、viewDidLoad から呼ばれたタイミングで Adapter の refresh メソッドが実行されるようになっています。refresh が呼ばれると最初に loading という Action がディスパッチされ、API の呼び出しが完了すると loading の Action がディスパッチされる仕組みになっています。

続いて DataModel、State、Mutation についてご説明します。Adapter が発行する Action は State のクラス内に定義されています。Mutation はその Action を受けて状態を更新する唯一の場所になっています。

さらに Computed を使って DataModel を ReactiveSwift のプロパティでラックすることで、ViewController のデータの変化を購読して UI を更新できるようになっています。こうした流れによってユーザー操作、Action 発行、State 更新、UI 反映といった単一方向のデータフローがシンプルに実装できます。

このViewFlexの考え方を踏襲しつつ、どのようにSwiftUIを使って実現できるかについて紹介します。

UIKitとVueのFluxの流れについて、シンプルにまとめると以下のような流れになります。まずユーザーが画面を表示したりボタンを押したりした際に、Vueコントローラーからadapterが呼び出されます。そのadapterがactionをディスパッチし、mutationがstateやdata modelを変更します。そして最後に、Vueコントローラーがその変更を購読してUIをリロードする仕組みとなっています。

SwiftUIでVue Flux的なアプローチを実装する場合の構成について説明します。SwiftUIのViewはユーザー操作を受け付け、ObservableObjectのメソッドを呼び出します。PublishのStateが更新されると、SwiftUIが画面を自動的に再描画する仕組みになっています。

UIKitの場合は購読してUIをリロードするコードを明示的に書く必要がありましたが、SwiftUIでは状態変更に応じて自動的にUIが更新されるため、より宣言的なコードを実現できます。ただし、Action、State更新、UI変更という単一方向のデータフローの基本的な流れは変わりません。

Vue Fluxにおけるアーキテクチャとの対応関係としては、Adapterの役割をSwiftUIのObservableObjectが担い、StateMutationの役割はPublishのStateとして実装されます。このStateの変更をトリガーとしてビューが更新される仕組みとなっています。

Model: では実際のコード例を見ていきましょう。

ObservableObjectやPublishedをどのように使っているか、実際のファイル構成とメソッドを通して説明していきます。

SwiftUIのビューでは2つの責務を分離して、ScreenとContentを用意します。画面の管理はScreenが行い、UIの細部はContentへ任せる構造となっています。まずScreenの部分についてご紹介します。Screenは親ビューとして、StateオブジェクトでAdapterを保持し、ナビゲーションやイベント制御などを担当する役割があります。

サンプルScreenのbodyメソッドでサンプルContentを呼び出しているのがわかります。ここでAdapterから渡されるDataModelをこのContentへ渡し、さらにユーザー操作はイベントクロージャーを介して受け取る形です。こうすることで、Screenでストアを保持し、ScreenからContentへデータを流す流れになります。

では、Contentについて説明します。Contentは実際のUIをレイアウトし、イベントをclosureでScreenに通知する役割を担当します。サンプルContentがDataModelを受け取り、画面を描画する仕組みになっています。

ユーザー操作が発生すると、didTapイベントのようにclosureを呼び出し、Screenへ通知します。

このように親から子へは状態を渡し、子から親はイベントを渡す単一方向のデータフローによって、このviewは状態を直接書き換えずにUIだけを担当できます。結果としてbindingなどを使わずに済み、コードの見通しが良くなるのが特徴です。

Adapterはobservable objectとしてpublishedなstateを持ち、actionを発行する役割を担います。サンプルAdapterによってloadMoreメソッドを持ち、非同期処理を行った結果をmutateメソッドで反映しています。こうすることで、View、Adapter、Stateという流れが明確になり、view側ではAdapterDataModelを読むだけで済みます。

Stateについて説明します。Stateはアプリケーションが保持するデータを一元管理し、Actionを受けてMutationファンクションで更新する仕組みです。サンプルのStateではDataModelプロパティを持ち、Actionごとにどのように書き換えるかを定義しています。例えばloadingのActionが来たらis loadingをtrueにし、loadedが来たら取得したデータをDataModelに追加するといった具合に、Mutation部分を明確に分離しています。これによりUIから状態を直接書き換えないため、一方向の流れを保ちやすくなります。

SwiftUIで実現したVueFlux-likeな状態管理の仕組みについて、実際の導入から得られた知見をご紹介します。

まず、大きなメリットとしてSwiftUIの恩恵を最大限に活用できる点が挙げられます。宣言的UIによって、VueFluxの時と比較してコードがよりシンプルになりました。例えばボタンのタップ時のUI更新処理も、状態変更に応じて自動的に更新されるため、明示的な処理を減らすことができます。

また、Storyboardが不要になったことでコードレビューの効率が格段に向上しました。これまでStoryboardの差分確認に時間を要していましたが、SwiftUIへの移行によってその課題が解消されました。

テストのしやすさ向上も重要なメリットです。Stateを独立してテストできるため、Actionが発行された際のデータ変換やロジックの検証が容易になりました。特に複雑な画面のロジックテストにおいて、UIの影響を受けずに状態管理部分だけをテストできる点は非常に有用です。

さらに、ライブラリ依存度の低さも大きな利点です。Redux系のフレームワークを導入せず、SwiftUIと組み合わせた独自のアーキテクチャを採用したことで、特定のライブラリに依存するリスクを回避できました。これにより、今後のSwiftのバージョンアップやAppleの仕様変更にも柔軟に対応できる設計となっています。

一方でデメリットもあります。

まず最初に boilerplate が増えることです。シンプルな機能でもひな形のコードが必要になります。例えばたった一つのデータを表示するだけでも、DataModel、State、Adapter、Screen、Content などの最低限の構造を整える必要があります。大型機能では Adapter が肥大化しやすいこともデメリットに挙げられます。非同期処理や画面遷移、外部とのやりとりをすべて Adapter で行っているため、機能が大きくなると Adapter に詰め込まれる処理が増えます。その結果、コードの見通しが悪くなります。

画面数が多いとコード量が増えます。このアーキテクチャは一つの画面でも5つのファイルが必要になります。そのため画面数が増えると当然ながらファイル数やコード量も大きくなり管理が難しくなります。

Model: 最後にまとめをお話しさせていただきます。私たちがSwiftUIを導入した理由は主に3つあります。1つ目は、新規参画するエンジニアがSwiftUIを学習している可能性が高いこと。2つ目は、Storyboardの差分確認の困難さから宣言的UIを検討したこと。3つ目は、既存のVueFluxによる単一方向の状態管理と整合性の取れるアーキテクチャを模索していたことです。

既存のアーキテクチャでは、VueFluxを採用し、Action、Mutation、State、ビューという流れでデータフローを実現していました。具体的には、Adapter、State、DataModelを組み合わせ、ViewControllerがUIを購読して更新する構成を取っていました。

SwiftUIでVueFlux-likeな状態管理を実現するにあたり、単一方向の状態管理を維持しながら、SwiftUIの特性に合わせた最適化を行いました。ScreenとContentという構成を採用し、ScreenでStateオブジェクトとしてストアを管理し、ContentはUIの表示とイベント通知に特化させました。Adapterはオブザーバブルオブジェクトとして実装し、パブリッシュのStateを持ち、mutating func mutateを使用することで単一方向のフローを実現しています。

この構成により、既存のVueFluxの設計思想を活かしながら、SwiftUIの利点を最大限に活用できる形に落とし込むことができました。以上で、SwiftUI導入から1年が経過した現在の、SwiftUI導入とVueFlux-likeな状態管理についての発表を終わりにさせていただきます。ありがとうございました。