New in iOS 15, we can form a Swift AttributedString like this:
var att = AttributedString("Howdy")
att.font = UIFont(name:"Arial-BoldMT", size:15)
att.foregroundColor = UIColor(red:0.251, green:0.000, blue:0.502, alpha:1)
print(att)
Cool, but there's another way. Instead of successive imperative property setting, we can make an attribute dictionary by way of an AttributeContainer, chaining modifier functions to the AttributeContainer to form the dictionary:
let att2 = AttributedString("Howdy",
attributes: AttributeContainer()
.font(UIFont(name:"Arial-BoldMT", size:15)!)
.foregroundColor(UIColor(red:0.251, green:0.000, blue:0.502, alpha:1))
)
print(att2)
(In real life I'd say .init() instead of AttributeContainer().)
So my question is, how does this work syntactically under the hood? We seem to have here a DSL where we can chain what look like function calls based on the names of the attribute keys. Behind the scenes, there seems to be some combination of dynamic member lookup, callAsFunction, and perhaps some sort of intermediate builder object. I can see that every callAsFunction call is returning the AttributeContainer, which is clearly how the chaining works. But just how would we write our own object that behaves syntactically the way AttributeContainer behaves?
I've made DSLs in the past similar to this.
I can't verify this is exactly what they're doing, but I can describe the way I achieved a similar DSL syntax.
My builder object would have methods like .font and .color return a temporary @dynamicCallable struct. These structs would store their parent build (by analogy, the AttributeContainer), and the keypath they were called originated from (\.font, \.color, etc.). (I don't remember if I used proper keypaths or strings. I can check later and get back to you.)
The implementation of callAsFunction would look something like:
func callAsFunction(_ someParam: SomeType) -> AttributeContainer {
parent[keyPath: keyPath] = someParam
return parent // for further chaining in the fluent interface.
}
Subsequent calls such as .foregroundColor would then repeat that same process.
Here's a bare-bones example:
@dynamicMemberLookup struct DictBuilder<Value> {
struct Helper<Value> {
let key: String
var parent: DictBuilder<Value>
func callAsFunction(_ value: Value) -> DictBuilder<Value> {
var copy = parent
copy.dict[key] = value
return copy
}
}
var dict = [String: Value]()
subscript(dynamicMember key: String) -> Helper<Value> {
return DictBuilder.Helper(key: key, parent: self)
}
}
let dict = DictBuilder<Int>()
.a(1)
.b(2)
.c(3)
.dict
print(dict)
IIRC, you can some generic magic and keypaths (instead of strings) to return different type per keypath, whose callAsFunciton could require arguments of different type, which can be enforced at compile time.
You can use @dynamicCallable instead of @dynamicMemberLookup+callAsFunction, but I don't think worked with the trick I just mentioned.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With