1. ホーム
  2. swift

[解決済み] SwiftUIのDSLを可能にするものは何ですか?

2022-12-04 18:26:40

質問

Appleの新しい SwiftUI フレームワークでは 新しい種類の構文 を使い、効果的にタプルを構築しますが、別の構文があります。

var body: some View {
    VStack(alignment: .leading) {
        Text("Hello, World") // No comma, no separator ?!
        Text("Hello World!")
    }
}

この構文が実際にどのようなものであるかに挑戦しています。 であることがわかりました。 VStack という型のクロージャを取ることがわかりました。 () -> Content を第二引数として受け取ります。 Content に準拠した一般的なパラメータです。 View に準拠した一般的なパラメータで、クロージャを介して推論されます。どのような型であるのかを知るには Content がどのような型に推論されるかを知るために、その機能を維持したまま、コードを少し変更しました。

var body: some View {
    let test = VStack(alignment: .leading) {
        Text("Hello, World")
        Text("Hello World!")
    }

    return test
}

これを使って test の型であることがわかります。 VStack<TupleView<(Text, Text)>> であることを意味します。 ContentTupleView<Text, Text> . 上を見ると TupleView から派生したラッパー型であることがわかりました。 SwiftUI に由来するラッパー型であり、ラップすべきタプルを渡すことによってのみ初期化することができます。

質問

今、私は一体どうやって2つの Text のインスタンスがどのようにして TupleView<(Text, Text)> . にハックされているのでしょうか? SwiftUI であり、したがって は無効な正規の Swift 構文ですか? TupleView であること SwiftUI タイプであることは、この仮定をサポートします。それとも、この はSwiftの有効な構文でしょうか? もしそうなら、どのようにして の外でそれを使うことができますか? SwiftUI ?

どのように解決するのですか?

マーティンが言うように のドキュメントを見ると VStack 's init(alignment:spacing:content:) を見ると content: パラメータには @ViewBuilder :

init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
     @ViewBuilder content: () -> Content)

この属性は ViewBuilder 型であり、生成されたインターフェイスを見ると、次のようになります。

@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
    /// through unmodified.
    public static func buildBlock(_ content: Content) -> Content 
      where Content : View
}

@_functionBuilder 属性は、" と呼ばれる非公式な機能の一部です。 機能ビルダー と呼ばれる非公式な機能の一部で、これは は、Swift の進化に投じられたものです。 そして、Xcode 11に同梱されているSwiftのバージョンのために特別に実装され、SwiftUIで使用できるようになりました。

型をマークする @_functionBuilder をつけると、関数や計算されたプロパティ、この場合は関数型のパラメータなど、さまざまな宣言でカスタム属性として使うことができます。このような注釈付きの宣言は、コードのブロックを変換するために関数ビルダを使用します。

  • アノテーションされた関数の場合、変換されるコードのブロックは実装です。
  • アノテーションされた計算されたプロパティのために、変換されるコードのブロックはゲッターです。
  • 関数型のアノテーションされたパラメータでは、変換されるコードのブロックは、それに渡される任意のクロージャ式です(もしあれば)。

ファンクションビルダがコードを変換する方法は、その実装によって定義されます。 ビルダーメソッド といった buildBlock のように、一連の式を受け取り、それらを一つの値に統合するものです。

例えば ViewBuilderbuildBlock 1から10まで View 準拠のパラメータを使用し、複数のビューを1つの TupleView :

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
    /// through unmodified.
    public static func buildBlock<Content>(_ content: Content)
       -> Content where Content : View

    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) 
      -> TupleView<(C0, C1)> where C0 : View, C1 : View

    public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
      -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View

    // ...
}

に渡されるクロージャの中で、ビュー式のセットを使用することができます。 VStack のイニシャライザに渡されたクロージャ内のビュー式の集合を buildBlock の呼び出しに変換されます。例えば

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
    }
  }
}

への呼び出しに変換されます。 buildBlock(_:_:) :

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
    }
  }
}

の結果 不透明な結果型 some View によって満たされる TupleView<(Text, Text)> .

ここで注目すべきは ViewBuilder のみを定義していることに注意してください。 buildBlock は 10 個までのパラメータしか定義できないので、11 個のサブビューを定義しようとすると

  var body: some View {
    // error: Static member 'leading' cannot be used on instance of
    // type 'HorizontalAlignment'
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
    }
  }

このコードブロックを処理するビルダーメソッドがないため、コンパイラーエラーが発生します (この機能はまだ作業中なので、エラーメッセージはそれほど役に立たないことに注意してください)。

実際には、この制限に遭遇することはそれほど多くないと思います。例えば、上記の例では ForEach を表示します。

  var body: some View {
    VStack(alignment: .leading) {
      ForEach(0 ..< 20) { i in
        Text("Hello world \(i)")
      }
    }
  }

しかし、10 個以上の静的ビューが必要な場合、この制限を簡単に回避するために Group ビューを使用することで、この制限を簡単に回避することができます。

  var body: some View {
    VStack(alignment: .leading) {
      Group {
        Text("Hello world")
        // ...
        // up to 10 views
      }
      Group {
        Text("Hello world")
        // ...
        // up to 10 more views
      }
      // ...
    }


ViewBuilder のような他のファンクションビルダのメソッドも実装しています。

extension ViewBuilder {
    /// Provides support for "if" statements in multi-statement closures, producing
    /// ConditionalContent for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures, 
    /// producing ConditionalContent for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View
}

これにより、if文を扱えるようになります。

  var body: some View {
    VStack(alignment: .leading) {
      if .random() {
        Text("Hello World!")
      } else {
        Text("Goodbye World!")
      }
      Text("Something else")
    }
  }

に変換される。

  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(
        .random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
                  : ViewBuilder.buildEither(second: Text("Goodbye World!")),
        Text("Something else")
      )
    }
  }

(への冗長な1引数呼び出しが発生します。 ViewBuilder.buildBlock への冗長な1引数呼び出しを行なっています)。