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
?
As Martin says, if you look at the documentation for
VStack
'sinit(alignment:spacing:content:)
, you can see that thecontent:
parameter has the attribute@ViewBuilder
:This attribute refers to the
ViewBuilder
type, which if you look at the generated interface, looks like: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
implementsbuildBlock
for 1 to 10View
conforming parameters, consolidating multiple views into a singleTupleView
:This allows a set of view expressions within a closure passed to
VStack
's initialiser to be transformed into a call tobuildBlock
that takes the same number of arguments. For example:gets transformed into a call to
buildBlock(_:_:)
:resulting in the opaque result type
some View
being satisfied byTupleView<(Text, Text)>
.You'll note that
ViewBuilder
only definesbuildBlock
up to 10 parameters, so if we attempt to define 11 subviews: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:If however you do need more than 10 statically defined views, you can easily workaround this restriction using the
Group
view:ViewBuilder
also implements other function builder methods such:This gives it the ability to handle if statements:
which gets transformed into:
(emitting redundant 1-argument calls to
ViewBuilder.buildBlock
for clarity).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: