Что позволяет SwiftUI DSL?

Кажется, что новая SwiftUI 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)>>, что означает, что Content имеет тип TupleView<Text, Text>. Просматривая TupleView, я обнаружил, что это тип оболочки, происходящий из самого SwiftUI, который можно инициализировать только путем передачи кортежа, который он должен обернуть.

Вопрос

Теперь мне интересно, как в мире два экземпляра Text в этом примере конвертируются в TupleView<(Text, Text)>. Это взломано в SwiftUI и, следовательно, неверный обычный синтаксис Swift? TupleView являющийся типом SwiftUI поддерживает это предположение. Или это правильный синтаксис Swift? Если да, то как можно использовать его вне SwiftUI?

Ответ 1

Как говорит Мартин, если вы посмотрите на документацию по VStack 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 evolution и реализована специально для версии Swift, поставляемой с Xcode 11, что позволяет использовать его в SwiftUI.

Маркировка типа @_functionBuilder позволяет использовать его в качестве пользовательского атрибута в различных объявлениях, таких как функции, вычисляемые свойства и, в этом случае, параметры типа функции. Такие аннотированные объявления используют конструктор функций для преобразования блоков кода:

  • Для аннотированных функций блок кода, который преобразуется, является реализацией.
  • Для аннотированных вычисляемых свойств блок кода, который преобразуется, является геттером.
  • Для аннотированных параметров типа функции блок кода, который трансформируется, является любым выражением замыкания, которое передается ему (если есть).

Способ, которым построитель функций преобразует код, определяется его реализацией методов построителя, таких как buildBlock, который принимает набор выражений и объединяет их в одно значение.

Например, ViewBuilder реализует buildBlock для от 1 до 10 View соответствующих параметров, объединяя несколько представлений в один 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 для ясности).

Ответ 2

Аналогичная вещь описана в разделе Что нового в видео Swift WWDC в разделе о DSL (начинается с ~ 31:15). Атрибут интерпретируется компилятором и переводится в связанный код:

enter image description here