cockscomblog?

cockscomb on hatena blog

SwiftUIではSingletonの代わりにEnvironmentを使うことができる

ここで言うSingletonというのは、ある種のグローバル変数を指している。そもそもクライアントアプリケーションの開発においては、実質的なグローバル変数が出現しやすい。環境にたった一つしか存在しない、存在すべきでない、というものが見出せる。例えばタイムゾーンがそれである。アプリケーション内のタイムゾーンはシステムに合わせるのが自然であり、アプリケーション中で複数存在することはほとんど起こり得ない。

iOSmacOSでは、タイムゾーンを表すのは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は追加できないが、幸いなことにEnvironmentValuesEnvironmentValues.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のような仕組みがしっかりと整備されており、高いポテンシャルを持っているように思う。

参考

SwiftUI 徹底入門

SwiftUI 徹底入門