/ SWIFT, SWIFTUI

Function Builders in Swift and SwiftUI

Functions builders is the language feature first introduced in Swift 5.1. It powers SwiftUI declarative DSL, which allows us to construct heterogeneous user interface hierarchies in a readable and concise way. In this article, we will learn how to utilize them in our code, covering the following topics:

  • What are function builders?
  • How do function builders work on the Swift compiler level?
  • How to implement a custom Swift function builder?

Understanding Swift Function Builders

A function builder is a type that implements an embedded DSL for collecting partial results from the expression-statements of a function and combining them into a return value [1].

The minimal function builder type implementation looks like:

@_functionBuilder struct Builder {
    static func buildBlock(_ partialResults: String...) -> String {
        partialResults.reduce("", +)
    }
}

A function builder type must be annotated with the @functionBuilder attribute, which allows it to be used as a custom attribute on its own.

Note that the function builder attribute has an underscore, meaning that the feature is currently in development.

The method buildBlock() is mandatory. It must be static and must have precisely this name. Otherwise, you’ll see a compilation error at a point of use.

A custom function builder attribute can be applied to [1]:

  1. A func, var, or subscript declaration which is not a part of a protocol requirement. It causes the function builder transform to be applied to the body of the function.
  2. A closure parameter of a function, including protocol requirements. It causes the function builder transform to be applied to the body of any explicit closures that are passed as the corresponding argument unless the closure contains a return statement.

Let’s continue with our @Builder example and review both usage scenarios.

We can use special syntax inside the declarations by marking them with the attribute @Builder:

@Builder func abc() -> String {
    "Method: "
    "ABC"
}

struct Foo {
    @Builder var abc: String {
        "Getter: "
        "ABC"
    }

    subscript(_ anything: String) -> String {
        @Builder get {
            "Subscript: "
            "ABC"
        }
        set { /* nothing */ }
    }
}

If we invoke the declarations:

print(abc())
print(Foo().abc)
print(Foo()[""])

It will print:

Method: ABC
Getter: ABC
Subscript: ABC

Moving to the second scenario, here is how we can pass a function-builder-annotated closure as an argument:

func acceptBuilder(@Builder build: () -> String) {
    print(build())
}

Then call the acceptBuilder() function with the DSL syntax enabled:

acceptBuilder {
    "Closure argument: "
    "ABC"
}

The above code will print:

Closure argument: ABC

The Purpose of Swift Function Builders

It’s always been a core goal of Swift to allow the creation of great libraries. [1]

The class of problems that Swift function builders solve is the construction of hierarchal heterogeneous data structures. Some examples are:

  • Generating structured data, e.g., XML, JSON.
  • Generating GUI hierarchies, e.g., SwiftUI, HTML.

That’s what it does. How it works?

Anatomy of Swift Function Builders

If we dump the generated AST from the abc() method:

(func_decl range=[builder.swift:10:10 - line:13:1] "abc()" interface type='() -> String' access=internal
...
  (declref_expr implicit type='(Builder.Type) -> (String...) -> String' location=builder.swift:10:31 range=[builder.swift:10:31 - line:10:31] decl=builder.(file).Builder.buildBlock@builder.swift:5:17 function_ref=single)
  ...
    (string_literal_expr type='String' location=builder.swift:11:5 range=[builder.swift:11:5 - line:11:5] encoding=utf8 value="Method: " builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
    (string_literal_expr type='String' location=builder.swift:12:5 range=[builder.swift:12:5 - line:12:5] encoding=utf8 value="ABC" builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
...

We’ll discover that it translates into the call to Builder.buildBlock("Method: ", "ABC").

During the semantic analysis phase, the Swift compiler applies the function builder transforms to the parsed AST as if we had written Builder.buildBlock(<arguments>) [1], [2].

Another usage case is when a function builder is applied to a closure parameter. In this case, the Swift compiler will rewrite the closure to a closure with a single expression body containing the builder invocations.

To be of some use, a function builder must provide a subset of seven building methods that implement different kinds of transformations [1], [2]:

  • buildBlock(_ parts: PartialResult...) -> PartialResult combines multiple partial results into one.
  • buildDo(_ parts: PartialResult...) -> PartialResult same as buildBlock(), but for the do clause.
  • buildIf(_ parts: PartialResult...) -> PartialResult same as buildBlock(), but for the if statement.
  • buildEither(first: PartialResult) -> PartialResult and buildEither(second: PartialResult) -> PartialResult create partial results from the result of either of two optionally-executed sub-blocks. You must implement both of the methods and they must be the inverse of each other.
  • buildExpression(_ expression: Expression) -> PartialResult creates a partial result from a single expression.
  • buildOptional(_ part: PartialResult?) -> PartialResult creates a partial result from the result of an optionally-executed sub-block.
  • buildFinalResult(_ parts: PartialResult...) -> Result produces a final result out of multiple partial results.

All of the methods support overloads based on their parameter types.

At a point of use, the Swift compiler will attempt to rewrite the DSL syntax using the provided subset of methods. In case that the compiler cannot not find a match, it will emit a compilation error.

Implementing Custom Function Builder

Let’s sharpen our knowledge by implementing an NSAttributedString function builder.

The builder creates a final NSAttributedString out of substrings:

@_functionBuilder
struct AttributedStringBuilder {
    static func buildBlock(_ components: NSAttributedString...) -> NSAttributedString {
        let result = NSMutableAttributedString(string: "")
        components.forEach(result.append)
        return result
    }
}

Next, we need to consider what types of expressions the builder supports. In our example, we will accept strings and images, and lift them to attributed substrings:

@_functionBuilder
struct AttributedStringBuilder {
    ...
    
    static func buildExpression(_ text: String) -> NSAttributedString {
        NSAttributedString(string: text, attributes: [:])
    }
    
    static func buildExpression(_ image: UIImage) -> NSAttributedString {
        let attachment = NSTextAttachment()
        attachment.image = image
        return NSAttributedString(attachment: attachment)
    }
    
    static func buildExpression(_ attr: NSAttributedString) -> NSAttributedString {
        attr
    }
}

Note that we also accept expressions of type NSAttributedString and return them unmodified.

We’ll also need a helper method that allows us to add extra attributes:

extension NSAttributedString {
    func withAttributes(_ attrs: [NSAttributedString.Key: Any]) -> NSAttributedString {
        let copy = NSMutableAttributedString(attributedString: self)
        copy.addAttributes(attrs, range: NSRange(location: 0, length: string.count))
        return copy
    }
}

Then add a builder-based convenience initializer:

extension NSAttributedString {
    convenience init(@AttributedStringBuilder builder: () -> NSAttributedString) {
        self.init(attributedString: builder())
    }
}

Lastly, let’s test the builder. Note that SwiftUI does not directly support NSAttributedStrings, hence we will resort to the good old UIKit:

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        label.attributedText = NSAttributedString {
            // 1.
            NSAttributedString {
                // 2.
                "Folder "
                // 3.
                UIImage(systemName: "folder")!
                // 4.
            }
            "\n"
            NSAttributedString {
                "Document "
                UIImage(systemName: "doc")!
                    .withRenderingMode(.alwaysTemplate)
            }
            .withAttributes([
                .font: UIFont.systemFont(ofSize: 32),
                .foregroundColor: UIColor.red
            ])
        }
    }
}

The Swift compiler translates the above code into the following AttributedStringBuilder method calls:

  1. buildExpression() with the NSAttributedString argument.
  2. buildExpression() with the String argument.
  3. buildExpression() with the Image argument.
  4. After all partial results have been built, the method buildBlock() is invoked with all intermediate substrings as arguments.

The result is next:

SwiftUI ViewBuilder Swift FunctionBuilder

Source Code

You can find the source code here. It is published under the “Unlicense”, which allows you to do whatever you want with it.

References


Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to not miss any new content.

Vadim Bulavin

Creator of Yet Another Swift Blog. Senior iOS Engineer at Pluto TV. Coding for fun since 2008, for food since 2012.

Follow