Mortar 3 (2025)

By the time SwiftUI had matured to the point where I was reaching for it on new projects, I had a long list of things I missed about UIKit — predictable performance, predictable layout, predictable everything. The declarative syntax of SwiftUI was genuinely nice, but the cost of giving up UIKit felt too high. So I decided to see if I could get both.

Mortar 3 is a complete rewrite. It shares basically nothing with Mortar 2 other than the name. The new DSL uses Swift’s result builders to express a UIKit view hierarchy in roughly the same shape you’d write a SwiftUI body:

view = UIContainer {
    $0.backgroundColor = .darkGray

    UIVStack {
        $0.alignment = .center
        $0.layout.sides == $0.parentLayout.sideMargins

        UILabel {
            $0.text = "Hello, World!"
        }

        UIButton(type: .roundedRect) {
            $0.setTitle("Button", for: .normal)
        }
    }
}

The views are real UIView instances — UILabel is a UILabel, UIVStack is a UIStackView underneath — so all of the usual UIKit machinery (animations, gesture recognizers, custom drawing) is still available. Layout uses an inline constraint syntax that compiles down to standard Auto Layout. Reactive bindings hook into CombineEx for things like bind(\.text) <~ model.title or handleEvents(.valueChanged, model.toggleAction).

It worked. It worked well, even. But I ended up walking away from it. As @Observable and the new Observation framework landed, I found I preferred designing screens around immutable observable view-models — the pattern I eventually settled into with SwiftAsyncEx and SwiftUIEx. Mortar 3 is still up if anyone wants the bridge between worlds, but I’m not actively using it on new code.

Source on github.