Часть 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 } }
Здесь немного происходит, но все следует той же логике, что и в абзаце выше.
- Во-первых, мы гарантируем, что у нас есть класс для объекта, на котором мы находимся, поскольку он возвращается как AnyClass?, иначе мы вернем пустой массив.
- Во-вторых, мы создаем переменную UInt32 для передачи в качестве входной переменной (обозначается амперсандом перед ее именем в методе).
- Наконец, мы вызываем созданный ранее метод свойств, передавая приведение 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