Question
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 outsideSwiftUI
?
Answer
[As Martin says](https://stackoverflow.com/questions/56434549/what-enables-
swiftuis-hovering-tuple-syntax#comment99463546_56434549), 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](https://github.com/apple/swift-
evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-
function-builders.md)", 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](https://github.com/apple/swift-
evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-
function-builders.md#function-building-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).