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

88

Похоже, что новая SwiftUIструктура Apple использует новый синтаксис, который эффективно строит кортеж, но имеет другой синтаксис:

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?

Фредпи
источник
3
developer.apple.com/documentation/swiftui/vstack/3278367-init показывает, что существует «настраиваемый атрибут» @ViewBuilder developer.apple.com/documentation/swiftui/viewbuilder .
Martin R
1
Обсуждается на форуме Swift здесь forum.swift.org/t/pitch-introduce-custom-attributes/21335 и здесь forum.swift.org/t/pitch-static-custom-attributes-round-2/22938 .
Martin R

Ответы:

109

Как говорит Мартин , если вы посмотрите документацию для 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 здесь и реализованного специально для версии 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")
      )
    }
  }

(выдача избыточных вызовов с ViewBuilder.buildBlockодним аргументом для ясности).

Хэмиш
источник
3
ViewBuilderопределяет только buildBlockдо 10 параметров - означает ли это, что var body: some Viewне может быть больше 11 подвидов?
LinusGeffarth
1
@LinusGeffarth На самом деле я не думаю, что люди будут сталкиваться с этим ограничением так часто, поскольку они, скорее всего, захотят использовать ForEachвместо этого что-то вроде представления. Однако вы можете использовать Groupпредставление, чтобы обойти это ограничение, я отредактировал свой ответ, чтобы показать это.
Хэмиш
3
@MandisaW - вы можете группировать просмотры в свои собственные и повторно использовать их. Я не вижу в этом проблемы. На самом деле, я сейчас на WWDC и разговаривал с одним из инженеров лаборатории SwiftUI - он сказал, что сейчас это ограничение Swift, и они выбрали 10 как разумное число. Как только универсальные вариативные типы будут введены в Swift, мы сможем иметь столько «подвидов», сколько захотим.
Losiowaty
1
Может быть интереснее, в чем смысл методов buildEither? Похоже, вам нужно реализовать оба, и оба имеют один и тот же тип возвращаемого значения, почему бы каждый из них просто не вернуть рассматриваемый тип?
Gusutafu
1
Следуя моему комментарию об ошибке ASTPrinter, это будет исправлено на главном сервере после объединения PR построителей функций .
Hamish
13

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

введите описание изображения здесь

Мацек Чарник
источник