Function builders are a new proposed feature in Swift that would allow for an expressive and descriptive way to create values. One of the new usages of this feature is in SwiftUI to effectively represent declarative UIs.
This is yet another post in a series of posts on some of the new Swift 5.1 features seen in Swift UI (introduced at WWDC19). I’ll discuss function builders if you’re not already familiar with them which are a pretty simple yet consise way to create data structures.
The first question is what are they? Function builders are just syntax sugar. What this means is that they are just a shorter syntax for what would otherwise be longer code:
Function builders don’t add new functionality but instead make your code easier to read, understand, and write.
Here’s an example of what a function-builder in action looks like:
div {
if useChapterTitles {
h1(chapter + "1. Loomings.")
}
p {
"Call me Ishmael. Some years ago"
}
p {
"There is now your insular city"
}
}
While the above example shows HTML being built, one of SwiftUI’s key features is function builders. If you want to learn and understand SwiftUI understanding function builders is a key step.
#Introduction
Swift function builders at the time of writing a proposed SEP (Swift Evolution Proposal). That said, they have been implemented in Xcode 11’s Swift 5.1 implementation.
#What does a function builder do?
Function builers sound like they create functions but they can create any value. It’s a function which lets you easily build complex objects like in the HTML examples above. You can attach a function builder to a class to build all sorts of objects. For example, I can have a function builder which produces a tuple or an array. You’ll see examples of these below.
One thing to note before we get started is now in Swift 5.1 returns are implicit if you only have one expression in a function. For example:
func getNum() -> Int {
4 // no need for 'return' keyword
}
#The Problem they Solve
Let’s take some examples of how some UIs would be declared in various languages. If you’re already familiar with building UIs in iOS you can skip this section because you’re probably acquainted with the boilerplate/difficulty of programmatically creating UIs. If you’re just learning Swift or you haven’t had much experience in UI programming I’d read over this section.
<div>
<p>Hello World!</p>
<p>My name is John Doe!</p>
</div>
If you’re not familiar with HTML this creates an element/node of type ‘div’ with two ‘p’ or paragraph elements in it. This example is pretty straightforward in how it declares the conent.
Well let’s see how we would create a UI like this in Swift before function builders:
let container = UIStackView()
container.axis = .vertical
container.distribution = .equalSpacing
let paragraph1 = UILabel()
paragraph1.text = "Hello, World!"
let paragraph2 = UILabel()
paragraph2.text = "My name is John Doe!"
container.addSubview(paragraph1)
container.addSubview(paragraph2)
A lot longer and harder to read compared to the HTML. This is where function
builders in the context of SwiftUI comes in. They offer the ability for
‘declarative UIs’. What does that mean? All that means is the code specifies
what the UI should look like not how it is achieved (whether it be manually
calculating positions of objects or using constraints)[^An example of this is
centering. You can specify the math for centering like (x - width)/2
or you could
tell the computer to set its position to center
. One is easier right?].
#Usage Example
Here’s an example of a function builder being used to create an array (hypothetical example):
@ArrayBuilder
func getAnArray() -> [Int] {
1
2
3
}
what this does is it uses the ArrayBuilder
class which is a
function builder and what it does is it takes each expression and creates a
value from them.
How does it solve the problem though? Let’s say I want to make the above component now I could potentially write something like (note this isn’t valid SwiftUI code but is conceptually similar):
@ViewBuilder
func createView() -> View {
Text("Hello, World!")
Text("My name is John Doe!")
}
What this would do is it would call a method from the ViewBuilder
class to
construct a new object (a new View
) from the expressions in the function
(the two Text
nodes).
#How they’re used in SwiftUI
Let’s take a deep dive into how this is used in SwiftUI (actual SwiftUI code):
let view = VStack {
Text("Hello, World!")
Text("My name is John Doe!")
}
What this is doing is it’s actually calling VStack
with a closure
containing the text. Another way to write this is:
let view = VStack({
Text("Hello, World!")
Text("My name is John Doe!")
})
now the question you may be asking is: how does swift know that closure is a
function builder? I never specified which function builder it uses. The answer is
that the VStack
constructor specified it. Let’s take a look at the VStack
constructor from Swift’s documentation:
As you cam see the initializer specifies that the closure is uses the
@ViewBuilder
function builder and this function builder produces an object of
type Content
(which is actually a subclass of View
).
Another way to think about it is that if function builders didn’t exist, I could write the exact same thing as above like the following:
let view = VStack({
return ViewBuilder.buildBlock(
Text("Hello, World!"),
Text("My name is John Doe!")
)
})
Here you can see the ViewBuilder
(type the function builder is) must have a
method named buildExpression
(function builders must have this name) and Swift
calls that method with all the expressions in the functions.
#Making your own Function Builder
To make your own function builder. You declare a class with the annotation
@functionBuilder
. In this example we’ll make a function builder which mimicks
the old UIView
functionality. What we want to do is create a function builder
which takes UIView elements and creates a wrapper UIView for them. The goal is
for something like:
@UIViewFunctionBuilder
func makeUIView() -> UIView {
uiTextView1
uiTextView2
}
and output a UIView containing both uiTextView
variables.
Let’s see the class declaration:
@functionBuilder
class UIViewFunctionBuilder {
}
Now the question is, how do we get this fucntion builder to take expressions and
produce a UIView
? Well it’s actually quite simple. All that we need to do is
declare a static function buildBlock
(name is important, if our method does
not have this name the function builder will not work). What that looks like is
this:
@functionBuilder
class UIViewFunctionBuilder {
static func buildBlock(_ children: UIView...) -> UIView {
let newView = UIView()
for view in expression {
newView.addSubview(view)
}
return newView
}
}
Note: at the time of writing you have to use
@_functionBuilder
because this is not currently an official member of the Swift spec yet.
The buildBlock
function will take the expressions as varargs (you can treat it
like a array) and then you must return a UIView
(which should contain all the
child views). What we do here is we create a new UIView()
and then just loop
through all the expressions and add them to this view.
In this example we only implement buildBlock
. This is all you really need to
implement but if you want to handle certain types of exceptions differently there
are a lot more functions you can implement, here’s the official list:
buildExpression(_ expression: Expression) -> Component
(Optional) is used to lift the results of expression-statements into the Component internal currency type. It is only necessary if the DSL wants to either:- distinguish Expression types from Component types
- provide contextual type information for statement-expressions.
buildBlock(_ components: Component...) -> Component
(Required) is used to build combined results for most statement blocks.buildFunction(_ components: Component...) -> Return
(Optional) is used to build combined results for top-level function bodies. It is only necessary if the DSL wants to distinguish Component types from Return types, e.g. if it wants builders to internally traffic in some type that it doesn’t really want to expose to clients. If it isn’t declared, buildBlock will be used instead.buildDo(_ components: Component...) -> Component
(Optional) is used to build combined results for do statement bodies, if special treatment is wanted for them. If it isn’t declared, buildBlock will be used instead.buildOptional(_ component: Component?) -> Component
(Optional) is used to build a partial result in an enclosing block from the result of an optionally-executed sub-block. If it isn’t declared, optionally-executed sub-blocks are ill-formed.buildEither(first: Component) -> Component
andbuildEither(second: Component) -> Component
(Optional) are used to build partial results in an enclosing block from the result of either of two (or more, via a tree) optionally-executed sub-blocks. If they aren’t both declared, the alternatives will instead be flattened and handled with buildOptional; the either-tree approach may be preferable for some DSLs.
Again, the only one of these functions that you need to implement is buildBlock
which takes the list of expressions and lets you write your own function which does
whatever you want with them.
Anyways, let’s take a look at the usage of our new function builder.
I’m also going to create a helper function (makeLabel
) just to make it cleaner:
func makeLabel(with text: String) -> UILabel {
let label = UILabel()
label.text = text
return label
}
@UIViewFunctionBuilder
func getView() -> UIView {
makeLabel(with: "Hello, World!")
makeLabel(with: "My name is John Doe!")
}
let view: UIView = getView() // like magic
And that’s it! You’ve just made a Function Builder which creates a UIView with two child UILabel elements.
Just to reiterate we could write the above as:
func getView() -> UIView {
return UIViewFunctionBuilder.buildBlock(
makeLabel(with: "Hello, World!"),
makeLabel(with: "My name is John Doe!")
)
}
#Summary
As you can see function builders aren’t that complex but are a very useful feature which allow you to write really descriptive and readable code. You may be wondering why you can’t just use an array and the answer is you can, but arrays look messy with all their brackets and parenthesis (this is what the proposal in fact says). Additionally, function builders are a simple but powerful feature and combined with SwiftUI will really change how us iOS/macOS developers make UIs.