110

It seems like Apple's new SwiftUI framework uses a new kind of syntax that effectively builds a tuple, but has another syntax:

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

Trying to tackle down what this syntax really is, I found out that the VStack initializer used here takes a closure of the type () -> Content as the second parameter, where Content is a generic param conforming to View that is inferred via the closure. To find out what type Content is inferred to, I changed the code slightly, maintaining its functionality:

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

    return test
}

With this, test reveals itself to be of type VStack<TupleView<(Text, Text)>>, meaning that Content is of type TupleView<Text, Text>. Looking up TupleView, I found it's a wrapper type originating from SwiftUI itself that can only be initialized by passing the tuple it should wrap.

Question

Now I'm wondering how in the world the two Text instances in this example are converted to a TupleView<(Text, Text)>. Is this hacked into SwiftUI and therefore invalid regular Swift syntax? TupleView being a SwiftUI type supports this assumption. Or is this valid Swift syntax? If yes, how can one use it outside SwiftUI?

fredpi
  • 8,414
  • 5
  • 41
  • 61
  • 3
    https://developer.apple.com/documentation/swiftui/vstack/3278367-init shows that there is a “custom attribute” `@ViewBuilder` https://developer.apple.com/documentation/swiftui/viewbuilder. – Martin R Jun 03 '19 at 21:15
  • 1
    Discussed in the Swift forum here https://forums.swift.org/t/pitch-introduce-custom-attributes/21335 and here https://forums.swift.org/t/pitch-static-custom-attributes-round-2/22938. – Martin R Jun 03 '19 at 21:19

2 Answers2

133

As Martin says, if you look at the documentation for VStack's init(alignment:spacing:content:), you can see that the content: parameter has the attribute @ViewBuilder:

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

This attribute refers to the ViewBuilder type, which if you look at the generated interface, looks like:

@_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
}

The @_functionBuilder attribute is a part of an unofficial feature called "function builders", which has been pitched on Swift evolution here, and implemented specially for the version of Swift that ships with Xcode 11, allowing it to be used in SwiftUI.

Marking a type @_functionBuilder allows it to be used as a custom attribute on various declarations such as functions, computed properties and, in this case, parameters of function type. Such annotated declarations use the function builder to transform blocks of code:

  • For annotated functions, the block of code that gets transformed is the implementation.
  • For annotated computed properties, the block of code that gets transformed is the getter.
  • For annotated parameters of function type, the block of code that gets transformed is any closure expression that is passed to it (if any).

The way in which a function builder transforms code is defined by its implementation of builder methods such as buildBlock, which takes a set of expressions and consolidates them into a single value.

For example, ViewBuilder implements buildBlock for 1 to 10 View conforming parameters, consolidating multiple views into a single 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

    // ...
}

This allows a set of view expressions within a closure passed to VStack's initialiser to be transformed into a call to buildBlock that takes the same number of arguments. For example:

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

gets transformed into a call to buildBlock(_:_:):

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

resulting in the opaque result type some View being satisfied by TupleView<(Text, Text)>.

You'll note that ViewBuilder only defines buildBlock up to 10 parameters, so if we attempt to define 11 subviews:

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

we get a compiler error, as there's no builder method to handle this block of code (note that because this feature is still a work-in-progress, the error messages around it won't be that helpful).

In reality, I don't believe people will run into this restriction all that often, for example the above example would be better served using the ForEach view instead:

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

If however you do need more than 10 statically defined views, you can easily workaround this restriction using the Group view:

  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 also implements other function builder methods such:

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
}

This gives it the ability to handle if statements:

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

which gets transformed into:

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

(emitting redundant 1-argument calls to ViewBuilder.buildBlock for clarity).

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 5
    *`ViewBuilder` only defines `buildBlock` up to 10 parameters* – does that mean `var body: some View` cannot have more than 11 subviews? – LinusGeffarth Jun 04 '19 at 09:55
  • 1
    @LinusGeffarth In reality I don't think people will run into this restriction all that often, as they'll likely want to be using something like the `ForEach` view instead. You can however use the `Group` view to workaround this restriction, I've edited my answer to show that. – Hamish Jun 04 '19 at 11:58
  • Thanks for teasing this out! Did you find any support so far for reusable custom (sub-)Views/Groups, or portable/parameterized builder logic? Otherwise I can't see this as being useful (read: maintainable) for anything beyond relatively simple layouts. – MandisaW Jun 04 '19 at 19:47
  • 4
    @MandisaW - you can group views into your own views and reuse them. I don’t see a problem with it. I’m at WWDC right now actually, and talked with one of the engineers at the SwiftUI lab - he said that its a limitation of Swift right now, and they went with 10 as a sensible-ish number. Once variadic generic get introduced into Swift, we will be able to have as many „subviews” as we want. – Losiowaty Jun 04 '19 at 23:28
  • Thanks for the clarification. So from my understanding, the `@_functionBuilder` can't have 2 functions with the same number of params. If that is true then can we get the compilation error if we make it by mistake, and if not, how it knows what functions to be used for the transformation. – sahara108 Jun 05 '19 at 12:50
  • Excellent info, thanks. But how come the signature of init looks different if you view it in XCode, there it has no Viewbuilder attribute: @inlinable public init(alignment: HorizontalAlignment = .center, spacing: Length? = nil, content: () -> Content) – Gusutafu Jun 11 '19 at 20:08
  • @Gusutafu I believe that's a compiler bug – the ASTPrinter hasn't been updated yet to print attributes for parameters. – Hamish Jun 11 '19 at 20:14
  • 1
    Maybe more interesting, what is the point of the buildEither methods? It seems like you need to implement both, and both have the same return type, why don't they each just return the type in question? – Gusutafu Jun 11 '19 at 20:15
  • OK, that's a relief. It confused me quite a bit earlier today when I tried to find where the builder actually was mentioned. – Gusutafu Jun 11 '19 at 20:20
  • @Gusutafu Good question regarding `buildEither`! I *believe* it's to help SwiftUI reason about when it needs to redraw content. For example, it [can deduce from type information whether certain views don't need redrawing](https://twitter.com/jckarter/status/1135684281328553985). By using `ConditionalContent` (which `buildEither` returns), presumably SwiftUI checks whether the branch taken is the same. If so, it doesn't need to redraw as long as no other state for the view on that branch has been mutated. – Hamish Jun 11 '19 at 20:39
  • 1
    Following up on my comment about the ASTPrinter bug, [this will be fixed on master once the function builders PR has been merged](https://github.com/apple/swift/pull/25221/files#diff-84ee3e2e9711400bf2749d0e807a7403R2593). – Hamish Jun 11 '19 at 20:40
  • Ah ok so while you always have the option to implement buildEither, the double generic one was strictly a SwiftUI thing. Maybe similar to why the return "some View", I think they prefer to have all the static knowledge about what particular View, that you would lose if you returned an existential. But your comment takes me back to my main question about SwiftUI, how the owning View can now that some State has changed. It sounds like State's storage is not on the State instance, but even if it is some singleton how can the View observe it change? – Gusutafu Jun 11 '19 at 20:58
  • 1
    Might help to refer to https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md which describes the principles in detail. – matt Jun 14 '19 at 20:26
  • 1
    @Hamish I also have a related question: is there a way to _see_ what any given SwiftUI code got transformed into after being passed through the builder? – matt Jun 14 '19 at 20:29
  • @matt Unfortunately, not that I know of. For the last example I did a `swiftc -dump-ast` in order to see the transformed AST, but that's pretty hard to read for non-trivial examples (and even for trivial examples, may take a little bit of knowledge about what the different nodes are for). It would be nice if sometime in the future Swift could support re-constructing readable source code from a type-checked AST, which would also let the user see things like synthesised protocol conformances. – Hamish Jun 14 '19 at 20:38
  • Starting from Xcode 11.3 something has changed in implementation of ViewBuilders? My code stop working correctly. public init(@ViewBuilder content: () -> TupleView<(A, B, C)>) { let views = content().value self.childs = [AnyView(views.0), AnyView(views.1), AnyView(views.2)] } above code no longer works, it needs to explicitly call ViewBuilder.buildBlock(), and @ViewBuilder invokes constructor for single Content view without Tuple – Michał Ziobro Dec 20 '19 at 14:27
15

An analogous thing is described in What's New in Swift WWDC video in the section about DSLs (starts at ~31:15). The attribute is interpreted by the compiler and translated into related code:

enter image description here

Maciek Czarnik
  • 5,950
  • 2
  • 37
  • 50