大変遅くなりましたが
2020年1月30日に開催されたCA.swiftへ
ブログ枠として参加させていただきました際の報告です。
https://cyberagent.connpass.com/event/155392/
ざっと内容や感想を書かせていただきます。
Thinking about Architecture for SwiftUI
登壇者
@d_dateさん
[KITASANDO COFFEE - 北参道コーヒー]
(https://apps.apple.com/jp/app/kitasando-coffee-%E5%8C%97%E5%8F%82%E9%81%93%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC/id1470570852)
スライド
内容
タイトルはSwiftUIの設計になっているけど
メインはDataflowの話(あと実は時間が30分だと思っていた)
### 宣言的にUIが記述できるフレームワークが流行っている
- SwiftUI
- Android: Jetpack Compose
- ReactNative/Flutter
SwiftUIとProperty Wrapper
@Stateなどで修飾されたプロパティと
ユーザーの操作をバインドして画面の状態を更新して再描画するという
Dataflowを構築している。
DataflowといえばCombine
- 宣言的に非同期なイベントを型として表現できるSwiftのAPI
- 様々なオペレーターでイベントをハンドリングできる
- Apple製
RxSwiftに取って代わるみたいな流れになっている。
SwiftUIと組み合わせて使われることが多い
CombineのPublisherをViewでSubscribeして
値が流れてくると
画面の状態の更新や画面の再描画を行う。
View with ObservedObjectを使った例
ObservableObjectプロトコルに適合したViewModelを
@ObservedObjectとしてViewで参照を保持して
SwiftUIのイベントとバインドさせて
単方向のデータの流れ(Unidirectional Dataflow)
を構築する。
Unidirectional Dataflowといえば
- Flux
- Redux
- Composable Architecture
FluxとReduxについては設計についての良い本があるのでそちらまで
Composable Architecture
pointfree.coで紹介。
- ReduxとElmアーキテクチャを組み合わせたもの
- SwiftUIとCombineに最適化(両方使ったアプリ作成を行っている)
- 関数型プログラミング
- View / State / Action / Store / Reducerを用いる
- 副作用はEffect型で全て定義
- 小さい部品を組み合わせて大きなデータフローを構築している
私も購読しているのですが
最初は仕組みがいまいちわからず
複数回見ないと全体の理解が難しかった印象を持っています。
ただ
全てがパターン化している上に
宣言的に記述できるので
中身をわかっている人には可読性が非常に高いなと感じています。
アプリの実装内容だけではなく
状態の変更を集約するという考えや
関数を型として扱う方法など
色々なことが学べるのでとても参考になると個人的には思ってます😃
簡単な流れ
- ActionをStoreに送る
- ReducerでActionをハンドリングしてStateを更新する
- Actionの結果Effect型で定義した副作用が発生する
- 更新されたStateを用いてViewを更新する
良さそうだけど、iOS13以降ではないと使えないし。。。UIKitとRxSwiftで同じことができないかやってみた
UIKit + RxSwift
- ユーザのアクションをSubscribeして流れてきた値をStoreに送る
- ReducerでActionをハンドリングする
- Effect型はiOSバージョン関係ないのでそのまま使え、Actionの結果Effect型で定義した副作用が発生する
- 更新されたStateを用いてViewを更新する
ただ一つのStore
Storeはアプリ全体で一つだけ。
理由として
- 認証情報などが引数で引き回す必要がなくなる
- 今後の情報の追加も楽
ただStoreが全てを管理していることで
- distinctUntilChangedしないと処理が何回も呼ばれてしまう
- 画面遷移時に初期化処理をしないと、前の画面の処理が動いてしまう
こともある(ここ何か聞き間違えていたかもしれません)。
SwiftUIとCombineで実現しているAppleの考えを
iOS13以前でも実現できることを具体的に知ることができて
大変勉強になりました。
AbemaTVにおけるiOSアーキテクチャの課題解決
登壇者
@_yysskkさん
スライド
内容
プロダクト背景
2015年3月から開発開始
現在は10名で開発
チーム人数の変遷
4(2015)
↓
6(2017)
↓
14(2019)
設計の変遷
Flux(第1世代)
↓
MVVM+Flux(第2世代) ← 縦化、ビデオ機能開始
↓
MVVM(Unio)+Flux(第3世代) ← アプリ内コイン、投げ銭機能開始
Flux(第1世代)
設計の選定理由
- 複雑な状態管理をわかりやすくしたい
- クライアント間(Android/iOS/Web)で同じ設計にしたかった
Fluxについて
https://facebook.github.io/flux/docs/in-depth-overview
大まかな処理の流れ
- ViewControllerがActionを呼ぶ
- ActionがAPI通信を行う
- APIから取得したデータをDispatcherに流す
- StoreでDispatcherから流れてくるイベントをbind
- ViewControllerがStoreの状態を監視してUIを更新
良い点
- View間の依存関係が減る
- 開発者の実装が統一されやすい
- データの流れがわかりやすい
MVVM+Flux(第2世代)
縦化とビデオ対応機能開始
第1世代のままだと困ること
- 機能の複雑化でStoreやUIの合成ロジックがViewControllerに増えた
例
Storeの値が更新された時にリロードしていたものに加えて
縦にした画面サイズの変更時もリロードする必要が出てきたため
combineLatestしなければならなくなった。
そこで
MVVMの導入
※
実際はRxを使用しているため
ViewModelではなくViewStreamという名前になっている
ViewModelのinitで
Inputのイベントを元にOutputを合成する
良い点
- ViewControllerをUIイベントの伝播と状態をUIにbindするだけにできた
- プレゼンテーションロジックをUIから分離したので単体テストを書くのが楽になった
- グローバルで保持する必要のない画面の一時状態をUIのライフサイクルで破棄できる
MVVM(Unio)+Flux(第3世代)
コイン機能の開発、投げ銭機能の開発
->チームメンバーの数が増えた
第2世代のままだと困ること
- ViewStreamの実装が実装者でバラバラ
例
テキスト検索を関数で定義するか
Observableのプロパティを使うか
PublishRelayを使うか
など
そこで
Unioの誕生(ViewModelの構造を統一)
https://github.com/cats-oss/Unio
良かった点
- ViewModelの実装の統一が実現
- ViewModelのロジックの流れが単一方向なので可読性が上がった
現在の課題
- 前世代の実装が残っている
- 第1世代の残っている実装でStore to Storeな実装があり処理が複雑になっている
- 画面の単体起動ができない
- Actionが直接Resourceを参照している
まとめ
変更を入れるタイミングは全機能の刷新や大きな機能開発が入る時に行う。
発生した課題に対して都度解決しながら変化していくことができた。
AbemaTVは比較的新しいので
最初の設計からシンプルでわかりやすいなと感じました。
変更に応じて設計を都度見直すというのも
変化の多い現在のアプリ開発ではとても自然な流れだと思っています。
ただ
前世代の実装が残っているということもおっしゃっていたので
やはりずっと負債になって手が出せないような部分も結構あるんだろうなとも感じ取れました。
そういったところに対して
どういう状態で手が出せずに
今後どうしていくのかといった点も聞いてみたいですね😃
Amebaの設計とこれから ~カオスからの脱却と再びカオスを生み出さない努力~
登壇者
スライド
内容
2017年中途入社
カオスすぎた設計
2010年にリリースされて以来
10年以上を運営する中でたくさんの転換
- WebViewアプリからフルネイティブに
- Objective-CからSwiftに
- メンバーの入れ替わり
- 当時デファクトと呼ばれて今は古くなっている設計
- リアーキテクチャを試みようとしたが道半ばになっている痕跡
基本はMVC
俗にいうMassiveViewController
ViewControllerからAPI通信を行っているものが多いものの
それはよくないという風潮からHogeHogeManagerなど名前から役割がいまいちわからない
クラスが作られていることもある。
テストはない。
なぜか一部VIPER
おそらくカオスなMVCから抜け出そうとして痕跡かもしれないが
ドキュメントがないのでいまいちわからない。
VIPERでもテストはない。
このままだと
新規画面を実装するにしても
- どの設計にすればいいか毎回0から考えて大変
- 人によって異なる設計をする状態でレビューが大変
- 設計をしたエンジニアがいなくなると後から変更加えるときに調査が必要になったり負担が重くなりそう
既存の実装に手を加える場合
- 設計がぼんやり&密結合&テストがないので何が正しいのかがわからず、工数が通常よりもかかってしまう状態
なのでリアーキテクチャ
方針として
- 最終的にはMVVM+Flux
- bindingとして利用するためにRxSwiftの導入も進める
- テスタビリティも考慮する
- CI環境を整えて自動テストを回していく
- 設計に関してはチームみんなで考える
特に最後の設計をチームで考えるが大事なことで
これがないとリアーキテクチャのモチベーション維持が難しくなる。
まずはMVVM
採用した理由として
- ロジックが複雑な画面がそこそこあった
- 画面間で同期が必要な仕様があまりなかった
bindingにはRxSwiftを利用
- 社内で使用しているプロジェクトが多かったため
初期のMVVM
ViewModelのinit内でPublishSubjectを生成して
ObserverとObservableのプロパティにセットして購読していた
簡単な画面ならこれで困ることはない
しかし
- Input,Outputのインターフェイスの形を実装者に委ねていたためレビューコストが肥大
- 複雑な初期化処理が入ってinitが肥大してメンテナンスコストが爆増
Usecaseの導入
ViewModelのinitで行っていた下記の処理をUsecaseに移動
- Observableの加工
- API通信
- ModelからView表示用Modelへ変換するTranslatorの呼び出し
MVVMの改良
- Input,OutputをProtocolに切り出しインターフェイスを統一
- initで行われていた処理を責務ごとに分離して肥大化の防止+テスタビリティの向上
※
2つ目に関しては
Usecaseに切り出したことで肥大化を防いだのか
また別で責務の分離を行ったのかは
確認できませんでした🙇🏻♂️
Flux導入
仕様変更によりMVVMの仕組みだと煩雑になってしまうようになった
例えば
- ユーザーのログイン、ログアウト状態によってアプリ全体の状態を変更
- あるタブの画面上の値の更新を別のタブの画面でも受け取らなければいけない
など
最初はObserverとObservableを持つ
Singletonのオブジェクトを利用して解決
しかし
Singletonで何でもできてしまうという点や
さらに監視する項目が増えた時のスケーラビリティが良くないと感じる点から
Fluxを導入
使い方は
- データフローはFacebookのものと同様
- StoreとViewModelを直接bindさせることで非Rx依存に(ExtensionでStoreをObservableに変換)
効果
- 利用者側は必要なイベントやStateを持つStoreを見ればよくなり、余計なObservableの購読がなくなった
- Singletonのときに感じていた処理の肥大化がする感じがしなくなった
- データフローが単方向でAPI通信が発生する処理でもキレイな構成を保てた
でも注意点も
利用箇所を考えずに入れてしまうと簡単な画面だとむしろ煩雑になってしまう可能性がある
使い所
MVVMではキレイに作れない処理の流れがあるような時に選択肢として利用
登壇者個人としては
NotificationCenterやObserverパターンで毎回独自の便利クラスを作るよりは
断然Fluxを使った方が良いと考えている。
現在までの結果(良かった点)
- 新規画面の実装は設計がある程度統一されているのでレビューが楽
- チームで認識を合わせて進めてきたので設計を考える大切さの共通認識が生まれた
- 設計ドキュメントを社内に残せるようになった
- テストを考慮したことでCI環境の見直しができた(メンテナー属人化したJenkinsからBitrise)
現在までの結果(悪かった点)
- 試行錯誤しながらだったため必要のない負債も生んでしまった
- もう少し検討する時間が必要だった
- 全画面でリアーキテクチャするまでのマイルストーンがまだ引けていない
これから
不安なこと
- ViewModelの先にあるUsecaseやRepositoryの作りをどうするか
- もし現メンバーがいなくなったらまた同じカオスを生み出してしまうのではないか
Layered Architectureの考え方を導入したい
しかし
人によってLayered Architectureの考え方が異なるので
Amebaに取り入れるにはどうしたら理想かを考えていきたい。
Layered Architectureとは?
https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html
まとめ
長くサービスを運営しているとカオスは生まれてしまうものの
この状況を伸びしろだとチームで捉えることができればどんどんリアーキテクチャを進めていった方が良いと思った。
設計に銀の弾丸はないので新しいものでもサービスの特性にマッチすればどんどん取り入れていきたい。
大事なのはチーム全体で設計へのコミットをしていくこと。
聞いていると
色々設計パターンが混ざっていて
コードを理解するのが大変そうだなと思いました。
そんな中で
どのように現在の動作を保証しながら行っているのか?
や
どのくらいの期間でどこまで達成できているのか?
といった話もぜひ聞いてみたいですね。
完全に達成するまでの道のりは険しそうですが
ここからどう変化しどこまで達成できたのかの次回の報告が楽しみですね😃
5年目に突入したAWAのアプリ設計
登壇者
スライド
内容
現在
v2.0
iOS9までサポート
iOSエンジニア3名
History
Objective-CからSwiftへ
開発体制の変更で人数など色々あったが
v1.0 -> v2.0で
フルスクラッチ開発
プロダクトの特性
- オフラインでも利用できる・再生できるようにするためにキャッシュが必要
- プレイヤーはどの画面からでもアクセスできる
など
v1の設計
MVCでRealmを利用。
問題点
- 2000~3000行のMassiveViewController
- Viewがかなり状態を持っている
- Controllerの責務が巨大
- Entityがロジックを持っている
- 万能なManager
- 神Util
- 全ての層でRealmに依存
v2でフルスクラッチ開発
背景
これまでの機能を全て見直し
全画面に手を入れる必要があったことから
作り直す良い機会だと捉えた
まっさらな状態から設計を考えることができた。
目標
- 責務の分離
- 状態の一元管理
- Realmへの依存からの脱却
- Reactive Programming
検討結果
Flux(with RxSwift) + レイヤードアーキテクチャー
やってみた結果
新規開発はやすくなったが
v1とv2の同時並行開発していたのでだいぶ大変だった。。。
加えていつ終わるのかが見えないので
モチベーションの維持が難しい。
Apple Watchにも対応
watchOS6.0以降対応ということで
MVVM with SwiftUI + Combine
で開発。
開発してみた結果
- まだまだ使えないライブラリが多い(XCTestやFirebaseなど)
- バイナリのサイズが大きくなるとアプリの転送速度がかなり遅い
- watchOSとSwiftUI起因のバグ(Betaだったからかもしれない)
これからの目標
- ViewControllerに状態やロジックを持っているものがあるのでそれを分離したい
- iOS/Android/PCで共通に利用できる共通の設計を導入する
- テストをもっとやりやすくする
v1とv2を同時に開発していたとおっしゃっていましたが
どっちの実装をしていたのか混乱しないのかなと
タスクやスケジュールの管理方法なども気になりました。
長年運用してきたアプリなので
仕様がいまいちわからない部分もあるのかなと思い
v1と同じ動作が期待される機能を
v2ではどうやって動作の保証をしていったのかなどの
話も聞いてみたいですね。
ディスカッション
自分でまとめようと思いましたが
とても良いまとめ記事がありましたので
そちらのリンクを貼らせていただきます🙇🏻♂️
最後に
大規模なアプリを長年運用していくにあたり
どのような状況でどういう判断をしていったのかが
具体的に伝わってくる内容ばかりで
自分がこういう状況にあったらどうしていただろうと
あれこれ考えながら楽しく聞くことができました。
そして
機能の変更や追加
人員の入れ替えなどがある中で
どうやって設計を保ち
どのようによりゴールに近づいていくのか
そういった部分への取り組みについても
聞いてみたいですね。
また
半年、1年後といった経過の報告を
聞く機会がありましたら
ぜひ参加したいなと思います。
運営のみなさま
登壇されたみなさま
素敵な会をありがとうございました😃