ここで言うSingletonというのは、ある種のグローバル変数を指している。そもそもクライアントアプリケーションの開発においては、実質的なグローバル変数が出現しやすい。環境にたった一つしか存在しない、存在すべきでない、というものが見出せる。例えばタイムゾーンがそれである。アプリケーション内のタイムゾーンはシステムに合わせるのが自然であり、アプリケーション中で複数存在することはほとんど起こり得ない。
iOSやmacOSでは、タイムゾーンを表すのはFoundation frameworkのTimeZone
である。システムのタイムゾーンはTimeZone.current
で取得できる。これはSingletonパターンのインターフェースに近い(本当にインスタンスが一つであるかどうかをここでは問題にしない)。
このように環境中で存在する個数が限定されるような値を表すのに、Singletonパターンが用いられることがある。しかしSingletonパターンを用いると、単体テストを書くのに不便だったり、あるいは多用することでコードベースのメンテナンス性が低下するなどとして、嫌われることも多い。このような問題は、DIのようなアプローチを用いることで軽減できるだろう。
SwiftUIのEnvironment
SwiftUIでは、タイムゾーンの取得に@Environment
を使う。以下のようにしてTimeZone
のインスタンスを得られる。
struct TimeZoneView: View { @Environment(\.timeZone) var timeZone var body: some View { Text("\(timeZone)") } }
@Environment
property wrapperを使い、EnvironmentValues
のpropertyをKeyPathとして(\.timeZone
のように)与えると、環境の値を読み取ることができる。
Environmentの上書き
環境の値は上書きすることもできる。以下のように、何らかのviewに対してenvironment(_:_:)
modifierを呼び出すと、そのviewの子孫の環境を上書きできる。
SomeView() .environment(\.timeZone, TimeZone(abbreviation: "GMT")!)
全てを上書きできるわけではなく、EnvironmentValues
のpropertyにset
が設定されているものだけを上書きできる。
タイムゾーンを上書きするのはあまり有意義とは言えないが、例えばEnvironmentValues.isEnabled
を上書きするのは理解しやすいユースケースである。特定のview以下を全てdisabledにするのは、リーズナブルだ。ということで、SwiftUIのviewにはdisabled(_:)
modifierがあり、実質的に.environment(\.isEnabled, false)
のショートハンドであろう。
新しいEnvironmentを作る
EnvironmentValues
を拡張してpropertyを追加することで、独自の値をEnvironmentに持たせることができる。extension
してもstored propertyは追加できないが、幸いなことにEnvironmentValues
はEnvironmentValues.subscript(_:)
によってストレージを提供してくれる。
例えばmacOSで、viewが所属するNSWindow
を環境から取得できるようにしてみる。(Singletonの例にはならないが……。)
struct WindowKey: EnvironmentKey { static var defaultValue: NSWindow? { return NSApp.mainWindow } } extension EnvironmentValues { var window: NSWindow? { get { return self[WindowKey.self] } set { self[WindowKey.self] = newValue } } }
このようにEnvironmentKey
に準拠した型を作って、EnvironmentValues
を拡張する。そして以下のように、NSHostingView
を初期化するときにNSWindow
への参照を与える。
let window = NSWindow(...) let contentView = ContentView() .environment(\.window, window) window.contentView = NSHostingView(rootView: contentView)
あとはいつでも@Environment(\.window) var window
として、NSWindow
にアクセスできる。
EnvironmentとDIコンテナ
こうしてみると、EnvironmentはViewをターゲットとしたDIコンテナのような役割を果たしている。これはObservableObject
を対象としたEnvironmentObject
でも同じだが、Environmentの場合はObservableObject
以外もその対象になる。
またEnvironmentは、ViewModifier
からも参照できる。EnvironmentalModifier
がそれだ。EnvironmentalModifier
は、それ自体がViewModifier
でありながら、resolve(in:)
メソッドを実装することで、EnvironmentValues
の値を元にして別のViewModifier
を解決することができる。つまりEnvironmentはViewModifierに対しても依存を提供できる。
DIコンテナとしては万能ではないが、これまでSingletonパターンを使っていたようなものの置き換えとしては十分だろう。
EnvironmentとContext
Reactに親しんでいれば、EnvironmentはReactのContextとほとんど同じということに気づくだろう。つまり、Reactでは内部的にContextを使って実現されるようなライブラリと似たようなことが、Environmentでも実現できそうだ。
まとめ
SwiftUIのEnvironmentを説明して、独自に拡張する方法を紹介した。そしてDIコンテナとの類似性から、Singletonパターンの置き換えとしての可能性を示した。
SwiftUIは昨秋にリリースされたばかりで、不完全な印象を抱きがちではあるが、Environmentのような仕組みがしっかりと整備されており、高いポテンシャルを持っているように思う。