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
?
In an embedded DSL, you use the built-in features of a host language like Swift to add the DSL's implicit behavior to some parts of your code, effectively modifying the host language into one tailored for your domain.
The @ViewBuilder attribute is one of the few result builders available for you to use in SwiftUI. You typically use it to create child views for a specific SwiftUI view in a readable way without having to use any return keywords.
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:
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With