Що дозволяє DSL SwiftUI?


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

Відповіді:


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")
      )
    }
  }

(випромінювання зайвих 1-аргументних дзвінків 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? Здається, вам потрібно реалізувати обидва, і обидва мають однаковий тип повернення, чому б їм не просто повернути відповідний тип?
Гусутафу,

1
Наслідуючи мій коментар про помилку ASTPrinter, це буде виправлено на майстрі після об’єднання PR конструкторів функцій .
Хаміш,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.