DEV Community

Gualtiero Frigerio
Gualtiero Frigerio

Posted on • Updated on

Create a DSL with Function Builders

Let me show something that you may be familiar with if you have done something with SwiftUI

let containerView = HStack {
    ForEach(names) { name in
        VStack {
            Text("hello \(name)").background(.yellow)
            if redLine {
                Text("second line is red").background(.red)
            }
            else {
                Text("second line is green").background(.green)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What if I tell you that this is not SwiftUI, but can be done with UIKit?
In this article I'm going to show you how to create a Domain Specific Language similar to SwiftUI by using Function Builders, one of the features of Swift that made SwiftUI possibile.
I've written another article about Function builders so you may want to have a quick read to that as well. The example I used in the article is really simple, but I go into more details about Function Builders.
This is the link of the GitHub project SwiftUIKit with all the code you can find in this article. It is not meant to be used in production, but is a funny exercise to build a DSL. I may have used HTML, but I thought mimicking SwiftUI would have been more interesting :)

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

Overview

Let's go back to the code I posted at the beginning. I said I'm using UIKit, to what are those HStack, Text and VStack?
It all starts with a protocol: SwiftUIKitView, you can see it here

protocol SwiftUIKitView {
    var type: SwiftUIKitViewType { get }
    var uiView:UIView? { get }
}
Enter fullscreen mode Exit fullscreen mode

So in the end, that's a container for a UIView, and we have a type

enum SwiftUIKitViewType {
    case empty
    case multiple([UIView])
    case single(UIView)
...
}
Enter fullscreen mode Exit fullscreen mode

So each View conforming to SwiftUIKitView can be empty, be a single view or a container for multiple UIViews.
Text, HStack, VStack, even ForEach, they all conform to SwiftUIKitView.
All of the struct has SwiftUIKit as prefix, so I used typealias to have Text, HStack etc. in the examples.

Function builder

Ok, we have a bunch of structs conforming to a protocol, but let's see how the "magic" of SwiftUI can be replicated in our example, by using a function builder. Again, there is my previous article for a basic explanation, so I'm getting right to the point here and show you the function builder, that you can find here on GitHub.

struct SwiftUIKitViewBuilder {
    static func buildBlock(_ views:SwiftUIKitView...) -> SwiftUIKitView {
        return SwiftUIKitContainer(withViews: views)
    }

    static func buildEither(first: SwiftUIKitView) -> SwiftUIKitView {
        return first
    }

    static func buildEither(second: SwiftUIKitView) -> SwiftUIKitView {
        return second
    }

    static func buildIf(_ view:SwiftUIKitView?) -> SwiftUIKitView {
        view ?? SwiftUIKitViewEmpty()
    }

    // buildIf has been replaced by buildOptional
    static func buildOptional(_ component: UIKitView?) -> UIKitView {
        component ?? EmptyView()
    }
}
Enter fullscreen mode Exit fullscreen mode

In my previous article I only covered buildBlock, by implementing this function you basically have an array of SwiftUIKitView and you are required to return a single SwiftUIKitView.

let containerView = HStack {
    Text("First text")
    Text("Second text")
}
Enter fullscreen mode Exit fullscreen mode

the function buildBlock in this example is called with two Text, and returns an object conforming to SwiftUIKitView. This is accomplished by the struct SwiftUIKitContainer, you can find it in the same file as the function builder. This struct takes an array of SwiftUIKitView and create a new one by setting the right type, single or multiple.

Conditional statements

In the first example I put an if, with a red view for the true branch and a green one for the false.
Take a look at the function builder, you can see two buildEither functions. Those are called when an if is found inside the closure, when the statement is true the function with first as the argument name is called, otherwise the second is called. The implementation is quite easy, you just return the view passed as an argument. That's all you need to do, if you don't provide buildEither, you can't use if...else.
You may have noticed there is a fourth function in my function builder: it is called buildIf.
This function is called when you put an if, but don't have an else statement. The function builder needs to know what to do, and that's why I implemented SwiftUIKitViewEmpty. As you can imagine, this is a SwiftUIKitView of type .empty, so no UIView. The function builder buildIf returns a SwiftUIKitView either way, but if the statement is false we don't have anything so we return an empty view.

Loops

Another important feature of a DSL like SwiftUI is the ability to have loop like ForEach inside the closure, in order to iterate a list of elements and show them like you'd do with a UITableView.
I've implemented ForEach in my DSL (see the code here ), it accepts an array of elements and you can have each element as a parameter of the closure providing the views, pretty much like SwiftUI. To keep the implementation simple I didn't consider the id, I just accept an array and pass each element to the function builder. You can use an Int, or a String, or another struct of your choice.

init(_ array:Array<Any>, @SwiftUIKitViewBuilder _ builder:(_ element:Any)->SwiftUIKitView) {
    var uiViews:[UIView] = []
    for element in array {
        let cycleView = builder(element)
        switch cycleView.type {
        case .single(let view):
            uiViews.append(view)
        case .multiple(let views):
            uiViews.append(contentsOf: views)
        default:
            ()
        }
    }
    if uiViews.count == 0 {
        viewType = .empty
    }
    else if uiViews.count == 1 {
        viewType = .single(uiViews[0])
    }
    else {
        viewType = .multiple(uiViews)
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the init function, taking an Array of Any and a function builder. Notice that the builder has a parameter, the element of the array.
For each element I call builder(element), what it does is calling the function builder (buildBlock, buildEither, buildIf) based on the closure passed to this function, and have a SwiftUIKitView back. At the end of the cycle I can set the type for the view, it will likely be multiple, but you may have a cycle with only one view, so I made the check.
That's it, the SwiftUIView produced by ForEach can be used as a parameter for the function builder of its container.

Modifiers

Another cool feature of SwiftUI is the chain of function you can call after declaring a view, like setting its frame, the background color, the font in case of a Text, etc.
Chaining functions is a concept I like, even outside a DSL like SwiftUI or the one I made for this article and I may come up with a separate post regarding that technique.
For now, let's talk about the modifier for my DSL, frame and background implementation are here

protocol SwiftUIKitModifier {
    func modify(_ view:SwiftUIKitView) -&gt; SwiftUIKitView
}

extension SwiftUIKitView {
    func modifier(_ modifier:SwiftUIKitModifier) -&gt; SwiftUIKitView {
        modifier.modify(self)
    }
}
Enter fullscreen mode Exit fullscreen mode

The extension of SwiftUIKitView contains the default implementation for modifier, so it isn't necessary to implement modifier in each one of them. It is straightforward, you call modify on a modifier passing self as the parameter. The modifier returns another SwiftUIKitView, this is why you can chain multiple modifiers together. Actually in my implementations I always return the same SwiftUIKitView, while SwiftUI creates another one, but that's an implementation detail. For the sake of the example it is ok to return the same struct, what is important is that the modifier always return a SwiftUIKitView, so you can chain multiple of them.

extension SwiftUIKitView {
    func background(_ color:UIColor) -&gt; SwiftUIKitView {
        let colorModifier = SwiftUIKitColorModifier(color)
        return colorModifier.modify(self)
    }

    func frame(_ frame:CGRect) -&gt; SwiftUIKitView {
        let frameModifier = SwiftUIKitFrameModifier(frame)
        return frameModifier.modify(self)
    }
}
Enter fullscreen mode Exit fullscreen mode

This extension of SwiftUIKitView allows me to call frame and background directly, instead of calling .modifier() on it. Looks cleaner and more similar to SwiftUI.

Text("hello world")
.modifier(SwiftUIKitFontModifier(UIFont.systemFont(ofSize: 30)))
.background(.yellow)

struct SwiftUIKitFontModifier: SwiftUIKitModifier {
    init(_ font:UIFont) {
        self.font = font
    }

    func modify(_ view: SwiftUIKitView) -&gt; SwiftUIKitView {
        if let textView = view as? Text {
            textView.setFont(font)
        }
        return view
    }

    private var font:UIFont
}
Enter fullscreen mode Exit fullscreen mode

Above an example of a modifier for a Font, called via .modifier on a Text. You could call it from another kind of view, and it would just return the view unmodified.
What if you wanted to call it in a more direct way, like frame and background? Let's see how to do that

Text("second line is red")
     .font(UIFont.systemFont(ofSize: 8))
     .background(.red)

extension SwiftUIKitText {
    func font(_ font:UIFont) -&gt; SwiftUIKitView {
        setFont(font)
        return self
    }
}
Enter fullscreen mode Exit fullscreen mode

The only downside is that you need to call .font as the first function, as it is relative to SwiftUIKitText. If you use .modifier you are free to put the call wherever you want.

Containers

In my example I implemented HStack, VStack and ZStack. They behave similar to the one in SwiftUI, you can put views in there and have them aligned horizontally or vertically.
This is the implementation of HStack

typealias HStack = SwiftUIKitViewHStack
struct SwiftUIKitViewHStack:SwiftUIKitView {
    var type: SwiftUIKitViewType {
        .single(containerView)
    }
    var uiView: UIView? {
        containerView
    }

    init(@SwiftUIKitViewBuilder _ builder:()-&gt;SwiftUIKitView) {
        let uiKitView = builder()
        containerView = FrameUpdateView()
        containerView.updateFrameHandler = updateFrameHandler
        if let uiViews = uiKitView.type.uiViews() {
            for uiView in uiViews {
                containerView.addSubview(uiView)
            }
        }
    }

    private var containerView:FrameUpdateView

    private func updateFrameHandler(_ containerView:UIView) {
        let size = containerView.frame.size
        let width = size.width / CGFloat(containerView.subviews.count)

        var frame = CGRect(x: 0, y: 0, width: width, height: size.height)
        for view in containerView.subviews {
            view.frame = frame
            frame.origin.x += width
        }
    }
}

fileprivate class FrameUpdateView:UIView {
    override var frame:CGRect {
        didSet {
            guard let updateFrameHandler = self.updateFrameHandler else {return}
            updateFrameHandler(self)
        }
    }

    var updateFrameHandler:((_ view:UIView)-&gt;Void)?
}
Enter fullscreen mode Exit fullscreen mode

The container is initialised with a function builder, no arguments this time (ForEach had one, remember?). I then create a FrameUpdateView and add all the UIViews coming from the function builder to it.
Why did I subclass UIView? To override the frame variabile. This way, I can be noticed every time the view's frame is modified, and I can call a function. What the function do is rearranging the subviews of FrameUpdateView based on the new frame. This way I can create an HStack, put views into it via the function builder and then call the .frame modifier on it. Whenever the frame changes on HStack, the views inside it are changed as well to have all of them horizontally arranged.
In SwiftUI you don't have to use a frame modifier on a HStack, but remember I have to get a UIView from the outermost container and add it to the hierarchy, so I need to set a frame.

Conclusion

As I stated at the beginning of the article this isn't a full fledged DSL ready for production. SwiftUI is, of course, way better than that.
I wrote it to have some fun with function builders and to demonstrate what is behind the "magic" of SwiftUI and some techniques like function chaining.
Hope you found it interesting and inspiring at the same time. Happy coding :)

Original post

Top comments (0)