Make a Function Builder

Get the Playground or Get the Subscription

Trying to get your SwiftUI code to compile

When 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:

  1. buildExpression(_ expression: Expression) -> Component

  2. buildBlock(_ components: Component…) -> Component

  3. buildFunction(_ components: Component…) -> Return

  4. buildDo(_ components: Component…) -> Component

  5. buildOptional(_ component: Component?) -> Component

  6. buildEither(first: Component) -> Component and buildEither(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 called buildIf

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.