Depending on how you did set up your function builder you can add an overload of buildExpression that takes Void as an argument and returns no result, e.g.:
static func buildExpression( _ statement: Void) -> [Expression] {
return []
}
This allows statements like this in function builder blocks:
self.label.alpha = 1
Or empty statements (e.g. in switch cases that should not do anything):
()
Edit: Fully working Solution
This is a slightly simplified version of what was requested (with label initialization filled in to make it compile):
class ViewController: UIViewController {
let label = UILabel(frame: .init(x: 30, y: 50, width: 200, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Hello World"
label.alpha = 0
view.addSubview( label)
Animation {
self.label.alpha = 1
OnEnd {
Animation {
self.label.frame.origin = .init(x: 120, y: 300)
}
}
}.startAnimation()
}
}
The Animation type would look like this:
struct Animation {
let block: () -> [OnEnd]
init(@AnimationBuilder block: @escaping () -> [OnEnd]) {
// save block for later execution
self.block = block
}
func startAnimation() {
var endBlocks: [OnEnd] = []
UIView.animate(withDuration: 3) {
// save OnEnd blocks for later execution and execute free statements
endBlocks = self.block()
} completion: { _ in
endBlocks.forEach { $0.run() }
}
}
}
OnEnd looks like this then:
struct OnEnd {
let block: () -> [Animation]
init(@OnEndBuilder block: @escaping () -> [Animation]) {
// save block for later execution
self.block = block
}
func run() {
block().forEach { $0.startAnimation() }
}
}
Now we need two function builders to make the above code work. One for the Animation:
@_functionBuilder
enum AnimationBuilder {
static func buildExpression(_ value: Void) -> [OnEnd] {
[]
}
static func buildExpression(_ value: OnEnd) -> [OnEnd] {
[value]
}
static func buildBlock(_ values: [OnEnd]...) -> [OnEnd] {
values.flatMap { $0 }
}
}
And one for completion:
@_functionBuilder
enum OnEndBuilder {
static func buildExpression(_ value: Void) -> [Animation] {
[]
}
static func buildExpression(_ value: Animation) -> [Animation] {
[value]
}
static func buildBlock(_ values: [Animation]...) -> [Animation] {
values.flatMap { $0 }
}
}
The end result looks like this:

Edit 2: Result of the transformation
For reference, this is the result of the compiler transformations that are triggered by the function builder attributes:
Animation {
let e0 = AnimationBuilder.buildExpression( self.label.alpha = 1)
let e1 = AnimationBuilder.buildExpression( OnEnd {
let e0 = OnEndBuilder.buildExpression( Animation {
let e0 = AnimationBuilder.buildExpression( self.label.frame.origin = .init(x: 120, y: 300))
return AnimationBuilder.buildBlock( e0)
})
return OnEndBuilder.buildBlock( e0)
})
return AnimationBuilder.buildBlock( e0, e1)
}.startAnimation()
Edit 3: Control Flow
It's probably a good idea to implement the rest of the builder methods to allow control flow statements (to the extent possible with function builders):
extension AnimationBuilder {
// Allow if statements that don't have an else clause
public static func buildOptional(_ elements: [OnEnd]?) -> [OnEnd] {
elements ?? []
}
// Allow if/else and switch statements
public static func buildEither(first elements: [OnEnd]) -> [OnEnd] {
elements
}
// Allow if/else and switch statements
public static func buildEither(second elements: [OnEnd]) -> [OnEnd] {
elements
}
// Allow for..in loops (in Swift 5.4)
public static func buildArray(_ elements: [[OnEnd]]) -> [OnEnd] {
elements.flatMap { $0 }
}
}
Same for OnEnd blocks:
extension OnEndBuilder {
public static func buildOptional(_ elements: [Animation]?) -> [Animation] {
elements ?? []
}
public static func buildEither(first elements: [Animation]) -> [Animation] {
elements
}
public static func buildEither(second elements: [Animation]) -> [Animation] {
elements
}
public static func buildArray(_ elements: [[Animation]]) -> [Animation] {
elements.flatMap { $0 }
}
}