If you like SwiftUI and appreciate the elegant syntax brought into SwiftUI by @ViewBuilder, you must know and love @resultBuilder in Swift that makes this magic happen. In this article, I would like to review this technology and explore how to create our own builders.
What is a resultBuilder
In Swift, resultBuilder is a special struct that allows us to implement a DSL(Domain Specific Language) to build a value from multiple values. Like @ViewBuilder builds a view from different views , where we just need to stack views together like below. The syntax is simple and declarative thanks to this DSL.
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}Where should resultBuilder be used
While @ViewBuilder in SwiftUI is a great use of this language feature, we, as developer, can also develop our own builders. But what problem it solves and when we should consider using this technology?
As its name implies, this technology is a builder pattern that builds final products from source materials. So anywhere we need to build a value from many other values and can benefit from this simplified, declarative syntax, we can consider adopting this technology.
Please note, if we purely want to build a value from multiple values, sometimes, creating an initialiser of the final value type is enough. For example, if we want to build a value of DateComponents , we can use this init method. However, it does seem not as elegant and flexible as resultBuild DSL method because we have to follow the exact order of the parameters etc. Feel free to have a look at this DateBuilder to gain more understanding of this.
How to develop a resultBuilder
If we make mind to develop a builder, let's look at how to design and implement one.
Building materials(components)
Firstly, think about building materials, which is what other value types needed for the final value.
Take the DateBuilder as an example, we want to build a Date value, for which, we need to know the Year, Month, Day, Time, Local, Calendar etc. We also might want to build a date directly from a String value that stands for a date already. All of those are the possible building components. In order to handle these with ease, we create a protocol to group these types together as our building materials like below:
public protocol DateComponent {}
extension Calendar: DateComponent {}
extension TimeZone: DateComponent {}
extension Date: DateComponent {}
...
// extracted from https://github.com/ShenghaiWang/SwiftMacros/blob/main/Sources/SwiftMacros/DateBuilder.swiftBuilding methods
Swift result builder offers the following methods. All of these methods do is to collect building materials, which is to collect values that needed for building the final value.
// Extracted from https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md
/// Required by every result builder to build combined results from
/// statement blocks.
static func buildBlock(_ components: Component...) -> Component { ... }
/// If declared, provides contextual type information for statement
/// expressions to translate them into partial results.
static func buildExpression(_ expression: Expression) -> Component { ... }
/// Enables support for `if` statements that do not have an `else`.
static func buildOptional(_ component: Component?) -> Component { ... }
/// With buildEither(second:), enables support for 'if-else' and 'switch'
/// statements by folding conditional results into a single result.
static func buildEither(first component: Component) -> Component { ... }
/// With buildEither(first:), enables support for 'if-else' and 'switch'
/// statements by folding conditional results into a single result.
static func buildEither(second component: Component) -> Component { ... }
/// Enables support for 'for..in' loops by combining the
/// results of all iterations into a single result.
static func buildArray(_ components: [Component]) -> Component { ... }
/// If declared, this will be called on the partial result of an 'if
/// #available' block to allow the result builder to erase type
/// information.
static func buildLimitedAvailability(_ component: Component) -> Component { ... }Among all of them, buildBlock is the only method that all resultBuilders need to implement. Others are optional depending on the needs. Our DateBuilder only implemented two of them.
public static func buildBlock(_ parts: [DateComponent]...) -> [DateComponent] {
parts.flatMap { $0 }
}
public static func buildExpression(_ expression: DateComponent) -> [DateComponent] {
[expression]
}Building result
Once we have collected all the building component, the final step is to build the final value based upon them, which is optional too. If not provided, the collected materials are the final result.
/// If declared, this will be called on the partial result from the outermost
/// block statement to produce the final returned result.
static func buildFinalResult(_ component: Component) -> FinalResult { ... }In our DateBuilder case, if we ignored this method, the final result would be [DateComponent] , which is not what we expected. So, we provide one implementation for this method like below and we get the final Date value from this builder.
public static func buildFinalResult(_ components: [DateComponent]) -> Date? {
var dateComponents = DateComponents()
components.forEach { component in
...
}
return dateComponents.date
}Conclusion
Swift resultBuilder is a great language feature that is not only powering SwiftUI, can also help in our codebase. With the DateBuilder we built, we can now generate date like below:
let date = buildDate {
DateString("03/05/2003", dateFormat: "MM/dd/yyyy")
Month(10)
Year(1909)
Hour(12)
Minute(30)
}As you can see, we firstly use a string to generate a date, then overwrite its month, year, hour, minute using a specified value.
You might think this is not useful and efficient at all. To some extent this is not a great builder. But as an example, I hope it helped to understand the concept of Swift result builder.
For your reference, feel free to check out URLBuilder and URLRequestBuilder.