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

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

59 голосов
/ 04 июня 2019

Похоже, что новый каркас 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?

Ответы [ 2 ]

75 голосов
/ 04 июня 2019

Как говорит Мартин , если вы посмотрите на документацию для VStack init(alignment:spacing:content:), вы увидите, что параметр content:имеет атрибут @ViewBuilder:

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

Этот атрибут относится к типу ViewBuilder, который, если вы посмотрите на сгенерированный интерфейс, будет выглядеть так:

<b>@_functionBuilder</b> 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
}

Это дает ему возможность обрабатывать, если сtatements:

  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).

4 голосов
/ 06 июня 2019

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

enter image description here

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...