Часть 2 из 3

История до сих пор

Если вы следили за первой частью этой сессии, то вы будете знать о коде, который мы создали до сих пор. В настоящее время у нас есть простой протокол, который позволяет нам создавать и обновлять экземпляры NSObject и все экземпляры подклассов NSObject:

protocol Updateable { }
extension NSObject: Updateable { }
extension Updateable where Self: NSObject {
    @discardableResult
    func update(completion: (Self) -> Void) -> Self {
        completion(self)
        return self
    }
}
let label = UILabel()
label.update {
    $0.text = "Hello World!"
}

В этом сеансе мы собираемся глубже погрузиться в создание копии или клона этого объекта, а затем реализовать обновление нового объекта. Имейте в виду, однако, что эта тема не для слабонервных, поскольку она углубляется в некоторые из более тонких и малоизвестных частей кода какао.

ПРИМЕЧАНИЕ. В качестве прелюдии к этому мы собираемся создать собственный протокол (Clonable), но мы также можем использовать NSCopying, если захотим. Во время этих руководств я хотел, чтобы протокол оставался чистым, без дополнительных требований соответствия, и поэтому выбрал свой собственный простой протокол.

Начиная

Давайте сначала позаботимся о самых простых частях. Первое, что мы хотим сделать, это создать новый экземпляр объекта, который мы собираемся клонировать. Для этой цели важно, чтобы у нас был какой-то инициализатор рассматриваемого объекта. Опять же, мы создадим новый протокол и дадим ему инициализатор.

protocol Clonable {
    init()
}

Еще раз мы назначим этот протокол NSObject (наш выбранный базовый класс):

extension NSObject: Clonable { }

Поскольку в NSObject уже есть init(), нам не нужно писать какой-либо код, чтобы привести его в соответствие. Хороший!

Теперь давайте добавим некоторый код расширения под привязку NSObject:

extension Clonable where Self: NSObject {
    var clone: Self {
        let clone = Self.init()
        return clone
    }
}

Это довольно простой код, верно? Все, что мы делаем, — это обращаемся к объекту Self, который, как мы знаем, имеет вызов init(). Мы инициализируем его и возвращаем новый объект. Потрясающий! Давайте попробуем:

class Person: NSObject {
    var firstName: String = ""
    var lastName: String = ""
    var age: Int = 0
}
let person = Person()
person.update {
    $0.firstName = "Paul"
    $0.lastName = "Napier"
    $0.age = 100
}
person2 = person.clone
print(person2.firstName)
print(person2.lastName)
print(person2.age)
// output: ""
// output: ""
// output: 0

Черт! На самом деле мы ничего не клонировали, мы просто создали новый экземпляр. Хм… Что делать?

Глубоко в недрах кода!

Я собираюсь показать вам некоторые скрытые аспекты кода, который мы используем. Большинству из нас никогда не нужно входить в эту область кода, но иногда бывает интересно заглянуть за кулисы, прежде чем вернуться в более безопасные земли нашего гораздо более высокого ландшафта разработки. Если это не ваша обычная зона комфорта, пожалуйста, не бойтесь… мы не задержимся здесь надолго! Если же вы уже разбираетесь в этом… Надеюсь, вам понравится!

Мы собираемся использовать что-то похожее на отражение, чтобы сгенерировать список доступных нам свойств, получить значения существующего объекта и, наконец, заполнить его в нашем новом клоне. И мы собираемся сделать это, используя безопасность типов Swift и перечисления :D

Под обложками Objective C завален непрозрачными указателями. Эти типы структур представляют собой указатели на типы, которые не могут быть представлены в Swift по разным причинам. Однако мы можем получить доступ к этим OpaquePointers, и мы это сделаем. В настоящее время нас интересует только свойство typeAlias ​​objc_property_t, представляющее свойство Objective C.

Однако сначала мы собираемся получить массив OpacquePointers из UnsafeMutablePointer:

extension UnsafeMutablePointer {
    func properties(length: Int) -> [OpaquePointer] {
        return UnsafeBufferPointer(
                 start: self, 
                 count: length).flatMap { $0 as? OpaquePointer}
    }
}

Приведенный выше код может показаться сложным, но все, что он делает, — это эффективное преобразование UnsafeMutablePointer через UnsafeBufferPointer в массив OpaquePointers. Вот и все. Мы оставим это там.

Каждый подкласс NSObject имеет список свойств, связанный с классом, который можно вернуть, вызвав метод class_copyPropertyList. Для этого нам нужен класс объекта, который мы хотим, а также переменная inout, которая будет заполнена ожидаемым количеством свойств. Итак, снова мы расширим NSObject, чтобы вернуть нам массив «objc_property_t» (псевдоним типа OpaquePointer, который мы видели выше).

extension NSObject {
    static var properties: [objc_property_t] {
        guard let classForKeyedArchiver = classForKeyedArchiver() 
        else { return [] } // 1
        var count: UInt32 = 0 // 2
        return class_copyPropertyList(
                    classForKeyedArchiver, 
                    &count).properties(length: Int(count)) // 3
    }
    var properties: [objc_property_t] {
        return type(of: self).properties
    }
}

Здесь немного происходит, но все следует той же логике, что и в абзаце выше.

  1. Во-первых, мы гарантируем, что у нас есть класс для объекта, на котором мы находимся, поскольку он возвращается как AnyClass?, иначе мы вернем пустой массив.
  2. Во-вторых, мы создаем переменную UInt32 для передачи в качестве входной переменной (обозначается амперсандом перед ее именем в методе).
  3. Наконец, мы вызываем созданный ранее метод свойств, передавая приведение count к стандартному типу Int. Затем, чтобы получить доступ к этому из нашего нашего класса, мы передаем удобный метод type(of:) и получаем свойства класса.

Если мы запустим это на экземпляре NSObject, то это будет отлично работать! Однако, если мы создадим подкласс, мы получим свойства только на верхнем уровне. Давайте решим это с помощью небольшой рекурсии.

extension NSObject {
    static var properties: [objc_property_t] {
        guard let classForKeyedArchiver = classForKeyedArchiver() 
        else { return [] }
        var count: UInt32 = 0
        var properties = class_copyPropertyList(
                              classForKeyedArchiver,
                              &count).properties(length: Int(count))
        if let parent = class_getSuperclass(classForKeyedArchiver)
                               as? NSObject.Type {
            properties.append(contentsOf: parent.properties)
        }
        return properties
    }
...
}

Я хочу сделать одну небольшую очистку прямо сейчас, а именно получить имя свойства в виде строки. Мы можем сделать это, расширив структуру objc_property_t.

extension objc_property_t {
    var name: String {
        guard let name = property_getName(self), 
              let string = String(utf8String: name) 
              else { return "" }
        return string
    }
}

Это просто берет указатель, получает для него имя свойства и возвращает это, если оно доступно. Если имени нет, то просто возвращает пустую строку.

Правильно! Вернемся к нашей переменной clone:

extension Clonable where Self: NSObject {
    var clone: Self {
        let clone = Self.init()
        for property in properties {
            guard case let name = property.name, 
                  let v = value(forKey: name) 
                  else { continue }
            clone.setValue(v, forKey: name)
        }
        return clone
    }
}

Здесь мы вставили цикл, который проверяет, имеет ли объект значение для имени свойства, и если это так, то оно устанавливает это свойство на клоне! Упс!

Как пройти крах

Но подождите... Попробуйте запустить этот код в Playground, и вы начнете видеть некоторые причудливые сбои, связанные с соответствием ключевому кодированию. Это происходит из-за того, что мы не соблюдаем атрибуты созданных нами свойств.

Подумайте об этом так. Если мы устанавливаем let, мы устанавливаем константу. Что-то неизменное и только для чтения. Однако здесь мы просто сообщаем новому классу, что независимо от атрибутов он должен установить значение, которое у нас есть, в качестве нового значения. Отсюда код-взрыв. Итак, в последней части мы собираемся создать механизм, который позволит нам определить, какие свойства мы хотим установить, а какие нет.

Следующая часть: https://medium.com/@rndm.com/update-objects-like-a-pro-in-swift-3-of-3-981286194015