Как работает цепочка функций 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, где мы можем связать то, что выглядит как вызовы функций, на основе имен ключей атрибутов. За кулисами, кажется, есть некоторая комбинация динамического поиска членов и, возможно, какой-то промежуточный объект построителя. Я вижу, что каждый callAsFunctioncall возвращает 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, но я не думаю, что сработал только что упомянутый трюк.

Другие вопросы по тегам