cockscomblog?

cockscomb on hatena blog

SwiftUIにおけるActionパターン

SwiftUIはWWDC20で大きく更新された。アプリ全体をSwiftUIで作る方法が確立されたほか、新しい抽象がいくつも導入されている。どれも非常に興味深いが、本記事ではその中からActionパターンを見出し、紹介する。

Actionの導入

WWDC20ではSwiftUIに、以下の4つの「Action」が導入された。

また対応するプロパティがEnvironmentValuesに追加されている。

ドキュメントに使い方が載っている。

struct SupportView : View {
    @Environment(\.openURL) var openURL

    var body: some View {
        Button(action: contactSupport) {
            Text("Email Support")
            Image(systemName: "envelope.open")
        }
    }

    func contactSupport() {
        openURL(mailToSupport)
    }
}

@Environment(\.openURL) var openURLとしてEnvironmentから取得しているのが、OpenURLAction構造体の値である。これをcontactSupport()関数の中で、openURL(mailToSupport)という風に呼び出している。

構造体の値を関数のように呼び出しているが、これは「SE-253 Callable values of user-defined nominal types」で議論され、Swift 5.2で導入された機能だ。このような値をcallable valueという。callAsFunctionメソッドを持っている構造体の値やクラスのインスタンスは、関数のように呼び出すことができる。関数のシグネチャcallAsFunctionメソッドのシグネチャと一致する。またcallAsFunctionメソッドはオーバーロードできる。

Actionという抽象

4つのActionが、「Action」と名付けられていることには意味がある。Actionの特徴は、以下のようになる。

  • Actionの名前はAction接尾語を持つ(XxxActionのように)
  • Actionの名前は動詞と目的語で構成される
  • Actionはcallable valueである(callAsFunctionメソッドを持っている)
  • ActionはEnvironment経由で取得できる
  • Actionを表すEnvironmentValuesのプロパティはActionの名前からAction接尾語を除いたものになる(OpenURLActionopenURLになる)
  • (おそらく)Actionは値型である

最後だけは「おそらく」と書いたが、Actionを参照型で作って状態を共有するようなケースはおおよそ考えられないし、またそうあるべきではないと思われるから、値型(つまり構造体など)で作るのが普通だろう。

これらの特徴を備えたActionは必然的に、コンテキストの親(祖先)が提供する機能を、子(子孫)から呼び出す、というユースケースになる。

独自のActionを作る

Actionとして抽象化できそうな処理があれば、独自のActionを作ることができる。

Actionとなる構造体を作るのは簡単だ。

struct PrintStringAction {
    func callAsFunction(_ string: String) {
        print(string)
    }
}

これをEnvironmentに入れるには、以下のようにKeyを作って、EnvironmentValuesを拡張する。

struct PrintStringActionKey: EnvironmentKey {
    static var defaultValue: PrintStringAction {
        return PrintStringAction()
    }
}

extension EnvironmentValues {
    var printString: PrintStringAction {
        get {
            return self[PrintStringActionKey.self]
        }
        set {
            self[PrintStringActionKey.self] = newValue
        }
    }
}

これだけで、@Environment(\.printString) var printStringとして利用できる。

(この例はActionとして抽象化するのが適切ではない……。)

SwiftUIにおけるAction

「Action」という語は、伝統的なCocoaにおいてはTarget-Actionパターンに登場し、さらにUIKitにはUIActionが登場している。あるいはFluxでも使われる。このように、文脈に依存して意味が少しずつ異なるが、SwiftUIではここまで説明したようなものが「Action」パターンである。

ということで、SwiftUIにおけるActionパターンを見出した。パターンというのは、再利用可能な設計のプラクティスである。

Actionパターンは、SwiftUIにおいて必要不可欠な抽象化になっていくだろう。