Как работает цепочка функций AttributeContainer в iOS 15?
В iOS 15 мы можем сформировать Swift AttributedString следующим образом:
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)
Круто, но есть другой способ. Вместо последовательной установки императивного свойства мы можем создать словарь атрибутов с помощью AttributeContainer, связывая функции-модификаторы с AttributeContainer для формирования словаря:
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)
(В реальной жизни я бы сказал
.init()
вместо
AttributeContainer()
.)
Итак, мой вопрос: как это работает синтаксически под капотом? Кажется, у нас есть DSL, где мы можем связать то, что выглядит как вызовы функций, на основе имен ключей атрибутов. За кулисами, кажется, есть некоторая комбинация динамического поиска членов и, возможно, какой-то промежуточный объект построителя. Я вижу, что каждый
callAsFunction
call возвращает AttributeContainer, что ясно показывает, как работает цепочка. Но как бы мы могли написать наш собственный объект, который синтаксически ведет себя так же, как AttributeContainer?
1 ответ
В прошлом я делал подобные DSL.
Я не могу проверить, что именно они делают, но могу описать способ, которым я добился аналогичного синтаксиса DSL.
У моего объекта-строителя были бы такие методы, как
.font
а также
.color
вернуть временный
@dynamicCallable struct
. Эти структуры будут хранить свою родительскую сборку (по аналогии,
AttributeContainer
), а путь к ключу, который они называли, возник из (
\.font
,
\.color
, так далее.). (Я не помню, использовал ли я правильные пути или строки. Я могу проверить позже и вернуться к вам.)
Реализация будет выглядеть примерно так:
func callAsFunction(_ someParam: SomeType) -> AttributeContainer {
parent[keyPath: keyPath] = someParam
return parent // for further chaining in the fluent interface.
}
Последующие вызовы, такие как
.foregroundColor
затем повторил бы тот же процесс.
Вот простой пример:
@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, вы можете использовать некоторые общие магические и ключевые пути (вместо строк), чтобы возвращать разные типы для каждого пути, чьи
callAsFunciton
может потребовать аргументов другого типа, которые могут быть применены во время компиляции.
Ты можешь использовать
@dynamicCallable
вместо
@dynamicMemberLookup
+
callAsFunction
, но я не думаю, что сработал только что упомянутый трюк.