DEV Community

Gualtiero Frigerio
Gualtiero Frigerio

Posted on • Updated on

Introduction to Function builders

SwiftUI has radically changed the way we build our UI, introducing a declarative syntax. Swift itself had to change in order to support some of the features of SwiftUI and this article is about one of them: function builders.
As a reference, this is the pitch on swift.org for the feature. The idea is to have a way to help building DSL (Domain Specific Language) in Swift in a declarative way, it was necessary for SwiftUI but we can use it in our own projects to have a similar syntax.

UPDATE: the feature finally made it to the language and is now called Result Builders. You don’t have to annotate builders with @_functionBuilder anymore but you can now use @resultBuilder

Function builders in SwiftUI

Let's take a look at a SwiftUI view

var body: some View {
    VStack(alignment: .leading) {
        Text("Sample text")
        Spacer()
        Text("Another text")
    }
}
Enter fullscreen mode Exit fullscreen mode

how is this possible? First, remember that Swift has the concept of trailing closure, it means that we can write the last parameter inside curly brackets. Let's find out what this last parameter is.

@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
Enter fullscreen mode Exit fullscreen mode

This is the init function for creating a VStack. @ViewBuilder is a function builder, something that takes all the parameters and merge them together to produce something, in this case a Content. To help you understand what a function builder does I'll use an example from the original proposal

// Original source code:
@TupleBuilder
func build() -> (Int, Int, Int) {
  1
  2
  3
}

// This code is interpreted exactly as if it were this code:
func build() -> (Int, Int, Int) {
  let _a = 1
  let _b = 2
  let _c = 3
  return TupleBuilder.buildBlock(_a, _b, _c)
}
Enter fullscreen mode Exit fullscreen mode

What happens with SwiftUI is similar. We don't have access to the source code, just to the headers, so the implementation you see below is an example

public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View {
// the function does something like this
    return TupleView(c0, c1)
}
Enter fullscreen mode Exit fullscreen mode

You can find many more of this buildBlock functions with generic types, as a matter of fact there are 10 versions of it, and this is why you cannot have more than 10 child views in SwiftUI, unless you put some of them into groups. 10 is just an arbitrary number, they could have made some more buildBlock functions and support up to 12, 16, 20 parameters but chose to implement up to 10.
There is a new proposal in Swift to support variadic generics to overcome this limitation, but for now we have to live with that.

Create a function builder

Ok, we had a first look at how SwiftUI gives us the ability to specify child views with its nice syntax.
Wouldn't it be great to build our view hierarchy with a DSL, just like in SwiftUI, using UIKit views?
I created a project on GitHub with a simple demonstration, using two structs, a view with a background color and a container capable of aligning its children horizontally or vertically.

override func viewDidLoad() {
    super.viewDidLoad()
    let view1 = SimpleView(withBackgroundColor: .yellow)
    let view2 = SimpleView(withBackgroundColor: .red)
    let frame = self.view.frame
    let containerView = ContainerView(frame:frame, align:.vertical) {
        view1
        view2
        SimpleView(withBackgroundColor: .blue)
        ContainerView(frame:frame, align:.horizontal) {
            SimpleView(withBackgroundColor: .orange)
            SimpleView(withBackgroundColor: .green)
        }
    }
    self.view.addSubview(containerView.view)
}
Enter fullscreen mode Exit fullscreen mode

This is the idea, we create a ContainerView pretty much like we'd do in SwiftUI and then we add it to our UIKit view. There is not SwiftUI here, although at a glance it is very similar. We can put variables or function calls into the trailing closure, and we can nest them so I can have a ContainerView inside another ContainerView.

This is what you see on screen: the outermost container is vertical, has 4 children so it divides the vertical space in 4 equal parts and puts the views there, the innermost container is horizontal and you can see the orange and green views have the same width. This is just a basic implementation, but I think you can get the point.

I defined a protocol so all of my views will conform to it. In SwiftUI there
is a body property of "some View", I used a protocol with a view property

protocol UIKitView {
    var view:UIView{get}
}
Enter fullscreen mode Exit fullscreen mode

In the example above you saw SimpleView and ContainerView, both conform to UIKitView so I can eventually get a UIKit object via the view property and add it to my UIKit hierarchy.
Let's take a look at how ContainerView is created, as you can imagine it needs a function builder

init(frame:CGRect, align:ContainerViewAlignment, @UIViewBuilder _ builder:()->[UIView]) {
    let views = builder()
    containerView = UIView(frame: frame)
    addViews(views, align: align, size: frame.size)
}
Enter fullscreen mode Exit fullscreen mode

Look at the third parameters, it is a function builder defined by a struct UIViewBuilder and it returns an array of UIView. ContainerView will add this child UIViews to its UIView so when you get the view property you have the container and its children, you can see the the code here
And finally, let's look at the function builder, this is the reason why you're reading this article after all :)

@resultBuilder
struct UIViewBuilder {
    static func buildBlock(_ views:UIKitView...) -> [UIView] {
        views.map{$0.view}
    }
}
Enter fullscreen mode Exit fullscreen mode

First, we need to write annotate the struct with @resultBuilder.
This syntax tells the compiler we are using the next struct called UIViewBuilder as a result builder, so you can later refer to it as @UIViewBuilder.
To keep things simple we deal only with buildBlock this time, there are a few more functions to handle optionals but I'll leave them to another article.
If you're not familiar with variadic parameters you may wonder about the purpose of the 3 dots after UIKitView. We're telling the compiler we expect a variable amount of parameters, and we get an array of them.
We expect the parameters to conform to UIKitView, so we know we can call the view property and get an UIView, that's why I can use map on the array of UIKitView and return an array of UIView.
As you can see the function builder is pretty simple, it is just a nice way to provide an array of parameters to a function. We already saw how to call the function builder, when we type let view = builder() in ContainerView we're calling buildBlock, and we get back an array of UIView.

Conclusion

That was a quick introduction to function builders. There is more to write about them and I plan to do it in the future.
Hopefully this article helped you understanding the "magic" behind SwiftUI and got you interested in leveraging the feature to build something nice in your projects.
Happy coding :)

Original post

You may be interested in my article about creating DSL with function builders

Top comments (0)