こんにちは!暑すぎてインドアで夏の全てを済ませているエンジニアの藤井(@touyou_dev)です!
毎年のWWDCでのアップデートから、次第に現実味をおびてきたプロダクションレベルでのAll SwiftUI採用アプリ。 昨年のiOS15でかなり安定してきて、今年リリース予定のiOS16では基本的なところはもちろんのこと、グラフなどもSwiftUI風に書けるようになっています。
これまではよほど小規模のアプリでない限り、あくまでUIKitをベースにすることが前提でなければいけないことも多く、アプリのアーキテクチャもUIKitでの慣習のまま。UI部分のみSwiftUIであるため、全体はMVVMで実装してDelegateパターンやObservableObjectのインスタンスをViewController側で持っておき、必要なデータをSwiftUIに渡すという形が一般的だったのかなと思います。ですが、全てをSwiftUIで実装するとなると、単純にUIKitの時のものを持ってくるわけにはいきません。
そこで登場したのが、The Composable Architecture(TCA)*1です。 これは宣言的UIフレームワークの先駆者であるReactでデファクトとなっているReduxを元に、Appleのエコシステムで使いやすいように作られたライブラリと、それによって実現できる設計手法のことを呼びます。
TCAは以下の記事などでも取り上げられ、今年の春にはプチバズが起こっていました。
- Qiita - 「SwiftUIでMVVMを採用するのは止めよう」と思い至った理由
- Qiita - 【SwiftUI】なぜ、MVVMをやめて、The Composable Architecture(TCA)を採用するのか?
宣言的UIフレームワークの特性を活かすために大事な、Composable(組み立て可能)な性質をもった完成度の高いアーキテクチャであるため、さまざまな人に注目されています。 ですが一方で、関数型の知識がかなり必要で学習コストが高かったり、まだ安定版になっていないなどデメリットもあげられています。
そこで今回の記事では、標準ライブラリのみで完結し、比較的理解しやすい方法でもう少しミニマムなアーキテクチャを考えられないかと思い、検討してみたことについて紹介したいと思います。
ミニマムなアーキテクチャ
アーキテクチャを考えると一口に言っても、そう簡単にいいものが何もないところから生み出せるとは限りません。 そこで、副業ではReactにも触れている自分の経験を活かし、今回はReactで使われているアーキテクチャに着目することにしました。
自分の関わっているReactのプロダクトは、ユーザー数が数万規模でいる立派なプロダクションレベルのサービスですが、そこまでグローバルな状態管理が複雑ではないため、基本的にはReduxなどを現時点で導入していません。 ではロジックなどをどう書いているかというと、React ContextとカスタムHooksを活用して書いています。
特にカスタムHooksは、似たようなロジックを別々の場所で書かなければならないとき非常に有用です。 全体のアーキテクチャはともかく、カスタムHooksでロジックを切り出すという考え方はわりと広く使われているのではないでしょうか?
そこで自分はこの考え方を導入することにしました。
どう導入するか?
Hooksという仕組みはReactの標準機能として発表されてから、さまざまな宣言型UIフレームワークに輸入されています。 実はSwiftUIでもこれは例外ではなく、SwiftUIにReactで提供されている標準的なHooks APIを導入するライブラリが日本のエンジニアによって制作されています。
これももちろんかなり有用ですが、一方でReactの基本的なHooksが関数型コンポーネント向けに設計されていることもあり、このライブラリも関数によってViewを表したときに真価を発揮するようなものになっています(もちろん構造体のままでも使えますが、Viewではなく専用のプロトコルに適合する必要があります。)
SwiftUIは基本的には構造体のViewに対してさまざまなAPIが設計されています。もちろんHooksの思想はとてもいいものなのですが、そのために標準の恩恵を受けにくくなってくるというのは少し本末転倒なところもあります。
そこで今回はこのようなライブラリや、特殊なプロトコルを用意するのではなく、あくまで考え方だけを導入するという方向で考えることにしました。
ObservableObjectをHooksとして見る
ようやくタイトル回収です。前節で「あくまで考え方だけを導入する」と書きましたが、どういうことなのかについて説明します。 まず最初に端的に言ってしまうと、それこそが「ObservableObjectをHooksとして見る」という見方です。
まだまだ発展途上であるため、世間のエンジニアの方々がObservableObjectをどう見ているかはあまり「〇〇である!」というようには言い切れないのですが、個人的な印象としてObservableObjectはViewModelやViewController側からデータを渡すためのStoreとして、1 Viewに対し1 ObservableObjectという使われ方をすることが多かったのかなと思います。
もちろんこのような使い方もわかりやすくていいのですが、結果的にViewModelとしてのロジックが肥大化してしまったり、それ自体では使い回しができないためにさらにロジック部分を抽象化してDIするといったような使われ方になっていたのではないでしょうか?
ですが、ここで書いた、特に「1 Viewに対し1 ObservableObject」という無意識のバイアスを取っ払って、SwiftUIのViewをReactの関数コンポーネントに照らし合わせて見るとObservableObjectが違った見え方をしてきます。
それこそがObservableObject = Hooksという見え方です。 簡単な例で比較図を作ってみました。以下はSwiftUIとReactそれぞれで、数字を1ずつ上げていくだけのコンポーネントを書いたものです。話の流れをわかりやすくするために、SwiftUIではあえてObservableObjectを、ReactではあえてカスタムHooks化して使ってみています。
細かいところを見なくとも、しっかりと対応関係が作れることが一目で見て取れるかと思います。
これはそれぞれの機能ができた理由を考えればすごく当たり前のことです。 どういうことか?そもそも、ObservableObjectもHooksも本来不変で副作用がない構造体と関数コンポーネントに対し、状態変化を扱えるようにするために設計されたものです。つまりObservableObjectとHooksは根本的に同じ目的に対する、それぞれのフレームワークに合わせた別解と言えるのです。
こう考えると先ほど書いた「1 Viewに対し1 ObservableObject」という考え方が非常にもったいないことをしているという感覚が生まれるのではないでしょうか? Reactで考えればこれは一つのコンポーネントに対し、そのロジックを全て一つのカスタムHooksにまとめているようなものです。たしかにそれだけでもロジックと見た目のDOMが分離されて、いくぶんか見通しが良くなりますが、扱う状態が多くなればなるほどカスタムHooksから返すべきものが多くなってきて余計扱いが難しくなってしまいますよね。 何よりカスタムHooksのうまみは別コンポーネントで似たようなロジックが必要な時に使いまわせることです。ですが、それを一つのコンポーネントのためだけにまとめてしまったら、そのコンポーネントへの依存度が強すぎて使い回すことなんて到底無理ですよね。
以上のことから、ObservableObjectをHooksと見て、コンポーネントに依存しないロジックと状態の独立した固まりとして扱うということが、ReactのカスタムHooksと同じメリットを受けるために非常に大事な考え方であることがうかがえます。 そしてそう見ることで、SwiftUIとReactの共通点というものをより実感を持って感じることができるのではないでしょうか?
とは言っても、ObservableObjectに関連する@ObservedObject
、@StateObject
、@Published
といったProperty Wrapperたちの考え方はSwift固有のものなのでどうしても特殊に見てしまう部分もあるかと思います。そこで最後に、よりReactとSwiftUIを共通して見れるようにする簡単な工夫を紹介しておきます。
命名を工夫する「HookObject」
最後にObservableObjectをHooksとして見る上で、意識的にそれを啓蒙できるようにする工夫を紹介します。 それがHooksのように扱うObservableObjectの接尾辞として、「HookObject」を使うという工夫です。 単純な命名規則なので、別に他のものでも構いません。今回自分は、Hooksが本来関数であること、ObservableObjectはあくまでクラスであることに着目し、そのどちらもわかるHook + Objectという命名を採用しました。
子供騙しのような工夫ではありますが、こうすることによって「これは再利用可能な単位で分割されたHooksのような存在なんだ」ということを名前から意識することができるようになります。さらに強くHooksというものを意識することによって、自分のようにReactの経験があるiOSエンジニアにとっては、どの単位で分割していくかの感覚をReactから輸入しやすくなるのではないでしょうか?そして何より、これはコンポーネントに依存せず使いまわせるものになるため、本当の意味でComposableな状態とロジックの管理方法と言えるのではないでしょうか。
長くなるのと、確立した論はあまり見たことがないため「じゃあどんな基準で切り出すんだ」というところはこの記事では割愛します。 あくまでSwiftUIの機能群を見るときの見方の一個として、参考にしていただければいいのかなと思います。
まとめ
今回この記事では、宣言的UIフレームワークの先駆者であるReactとの共通点を見出し、ObservableObjectとカスタムHooksの共通点に着目することで、ミニマムに標準ライブラリのみで状態やロジックを扱うアーキテクチャの考え方を紹介しました。 蓋を開けてみると、標準ライブラリを本来の目的で扱っているだけにすぎない記事にはなったと思います。ですが、その本来の目的が十分にSwiftUIユーザーに伝わっていない、というのもまた一個の事実だと思います。
ぜひ一個考え方の参考として、みなさんもこの「HookObject」を使ってみてください。
Goodpatchではデザイン好きなエンジニアの仲間を募集しています。 少しでもご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう!