Make a Function Builder
Get the PlaygroundorGet the SubscriptionWhen I first saw SwiftUI
, I instantly wondered how it was possible. It didn't look like Swift. This foreign syntax was extremely expressive compared to UIKit
. What used to take a ridiculous amount of UIKit
code and possibly a storyboard, now takes 6 lines of code:
List(todos, id: \.id) { todo in
HStack {
Image(systemName: "checkmark.circle\(todo.completed ? ".fill" : "")")
Text(todo.title)
}
}
SwiftUI
's declarative syntax is made possible by function builders (here's the proposal). This is what allows you to combine and compose views together (e.g., HStack
taking two Views
and forming a TupleView
). Function builders are a huge step forward in Swift supporting declarative programming, and provide the basis for tons of powerful new DSLs.
I've been messing around with them lately, and I recently released a DSL for making HTTP requests. I learned a lot about function builders along the way. I hope the information I've included here can help you create your own DSLs.
Using a Function Builder
Let's take a look at using the ViewBuilder
that powers SwiftUI's DSL:
@ViewBuilder
func combineWords() -> TupleView<(Text, Text)> {
Text("Hello")
Text("World")
}
combineWords()
Let's break it down:
The @ViewBuilder
function builder is applied to a function that converts two Text
views into a TupleView
that contains both Text
views.
We don't need a return statement because of SE-0255, which allows for implicit returns.
What the compiler does is convert our function into this:
func combineWords() -> TupleView<(Text, Text)> {
let _a = Text("Hello")
let _b = Text("World")
return ViewBuilder.buildBlock(_a, _b)
}
As you can see, ViewBuilder
does all the heavy lifting. It takes in our views and creates the TupleView
. You can use your own function builders in the same way. Let's look at an implementation of a function builder.
@_functionBuilder
struct GreetingBuilder {
static func buildBlock(_ items: String...) -> [String] {
items.map { "Hello \($0)" }
}
}
As of Swift 5.1 in Xcode 11 beta 4, we need to use
@_functionBuilder
instead of@functionBuilder
. This will be changing once the full proposal is implemented and accepted.
What our function builder does is take in strings, and return those strings with "Hello"
inserted at the beginning.
When we use our function builder, the buildBlock
function is called, and it takes in any Strings
we pass. This is due to the variadic parameter String...
You can learn more about variadic parameters in the Swift docs
Inside the buildBlock
function we can perform any logic we need to create our String
array. In this case, we add "Hello"
to the beginning.
Creating a Function Builder
There are endless possibilities when it comes to function builders we could make. I decided a good way to start would be something simple and useful: NSAttributedString
.
In Swift, creating an attributed string can be a real pain. Wouldn't it be nice if we could use SwiftUI
style syntax?
NSAttributedString {
"Hello "
.color(.red)
"World"
.color(.blue)
.underline(.blue)
}
We can create a function builder to do just that. Let's start with our builder: NSAttributedStringBuilder
@_functionBuilder
struct AttributedStringBuilder {
static func buildBlock(_ segments: NSAttributedString...) -> NSAttributedString {
let string = NSMutableAttributedString()
segments.forEach { string.append($0) }
return string
}
}
Here we define our function builder and the standard buildBlock
. In this case, we take in multiple attributed string "segments". Then we loop through them to create one combined string.
You can treat
NSAttributedString...
as an array
We want our function builder to work when initializing an NSAttributedString
so we need to create a convenience initializer.
extension NSAttributedString {
convenience init(@AttributedStringBuilder _ content: () -> NSAttributedString) {
self.init(attributedString: content())
}
}
Now we're ready to use our builder. However, to make the process even more seamless, I've created extensions for String
and NSAttributedString
to add modifiers. You can get the code for those here.
Ok, let's try out our function builder:
NSAttributedString {
"Hello "
.foregroundColor(.red)
.font(UIFont.systemFont(ofSize: 10.0))
"World"
.foregroundColor(.green)
.underline(.orange, style: .thick)
}
That's all it takes to create a function builder. However, you may notice this doesn't work in certain instances. When using SwiftUI
you may do something like this:
HStack {
if greeting != nil {
Text("\(greeting!) ")
}
Text("World")
}
However, if you try to do something similar with our function builder, you'll run into this error:
ERROR: closure containing control flow statement cannot be used with function builder 'AttributedStringBuilder'
More Build Functions
To extend the functionality of your builder, you need to implement more static functions. The full proposal will support the following:
buildExpression(_ expression: Expression) -> Component
buildBlock(_ components: Component…) -> Component
buildFunction(_ components: Component…) -> Return
buildDo(_ components: Component…) -> Component
buildOptional(_ component: Component?) -> Component
buildEither(first: Component) -> Component
andbuildEither(second: Component) -> Component
I will update this article as these become available. Currently, we can use buildBlock
and buildOptional
buildOptional
As of Xcode 11 beta 4,
buildOptional
is calledbuildIf
Build optional allows optional values to be included in the block. This includes if statements, which upon being false will return nil
.
Here's a simple implementation to handle the possible nil
@_functionBuilder
struct AttributedStringBuilder {
...
static func buildIf(_ segment: NSAttributedString?) -> NSAttributedString {
segment ?? NSAttributedString()
}
}
Wrapping Up
So there you have it. Swift 5.1 function builders (in their current state). I can't wait to see what DSLs pop up in the next few months. If you make something cool, add it to the list of awesome-function-builders. I'm excited to see what you all create!
You can view the finished code here.