NSKeyValueObserving

Ruslan Lutfullin
Aug 28, 2017 · 6 min read

Доброго времени суток, Дорогие друзья! (нет)

Сегодня мы поговорим о такой вещи, как Key-Value Observing (KVO). Key-Value Observing это важная концепция, которой очень немногие разработчики пользуются. API этой концепции достаточно прост. Он позволяет уведомлять объекты при изменении состояния другого объекта. Это звучит очень полезно!

Key-Value Observing реализуется посредством протокола NSKeyValueObserving. Этот протокол, как указано в документации 🍏, является неофициальным. Класс NSObject уже реализует протокол NSKeyValueObserving и любой класс, который наследуется от NSObject также реализует этот протокол. Все классы в Foundation очевидно реализуют этот протокол, а вот классы(большинство) в UIKit не реализуют этот протокол. Но это можно исправить 😬.

Давайте я покажу Вам работу c Key-Value Observing на примере. Зайдём в Xcode и создадим Single View Application. После создадим два файла(класса), которые будут наследоваться от NSObject:

Имплементируем Configuration:

Также имплементируем ConfigurationManager:

ConfigurationManager управляет экземпляром класса Configuration. Класс Configuration определяет два свойства, createdAt и updatedAt. Оба имеют тип Date. Откройте класс ViewController и определите свойство для ConfigurationManager и outlet для экземпляра UILabel.Также укажите action для UIButton , который будет собственно и обновлять дату.

А в Storyboard должно получиться что-то такое:

Основная идея Key-Value Observing проста. Когда объект добавляется в качестве наблюдателя для определенного #keyPath, он получает уведомление, когда свойство, за которым он наблюдает — изменится. Несмотря на то, что API протокола NSKeyValueObserving невелик, он немного улучшился в Swift 3.

Смысл в том, чтобы обновить значение метки UILabel при изменении значения свойства updatedAt. Это означает, что ViewController должен быть уведомлен об изменении. На языке Key-Value Observing нам нужно добавить ViewController в качестве наблюдателя. Обновите метод viewDidLoad() класса ViewController, как показано ниже.

В viewDidLoad(), ViewController добавляет себя в качестве наблюдателя для свойства updatedAt экземляраConfiguration. Он делает это, вызывая addObserver(_: forKeyPath: options: context :), метод класса NSObject. Поскольку класс UIViewController наследуется от NSObject, этот метод доступен нам.

Метод addObserver(_: forKeyPath: options: context :) определяет четыре параметра.

_: первым параметром является объект, который добавляется в качестве наблюдателя. Это может быть только экземпляр класса, который наследуется от корневого класса NSObject. Это корневой класс NSObject, который определяет метод addObserver(_: forKeyPath: options: context :), а также метод, который вызывается при обнаружении изменения.

forKeyPath: определяет, каким свойством интересуется наблюдатель. В нашем примере ViewСontroller добавляется в качестве наблюдателя для свойства updatedAt экземпляра класса Configuration, который в свою очередь содержится в экземпляре класса ConfigurationManager в ViewСontroller. Сложно? Посмотрим на путь ещё раз? Вот он #keyPath(configurationManager.configuration.updatedAt).

Для определения пути мы используем выражение #keyPath. Ключевой путь — это не что иное, как последовательность свойств объекта. До Swift 3 ключ был строковым литералом. Благодаря добавлению выражений #keyPath, компилятор может проверить правильность пути ключа во время компиляции. Строковые литералы не имеют этого преимущества, что часто приводит к ошибкам.

Если путь ключа считается действительным, компилятор заменяет его строковым литералом во время компиляции. Зачем? Key-Value Observing использует Objective-C runtime. В Objective-C ключи и пути ключей представлены строками. И помните, что Key-Value Observing возможно использовать только потому, что Swift использует Objective-C runtime.

Обратите внимание, что #keyPath, используемый в addObserver(_: forKeyPath: options: context :), относится к текущей области и контексту. Смотрите сами, #keyPath(configurationManager.configuration.updatedAt) , а не #keyPath(СonfigurationManager.configuration.updatedAt).

options: вы можете дополнительно передать список опций addObserver(_: forKeyPath: options: context :). По умолчанию используется пустой набор параметров. Список опций определяет, какую информацию предоставляет наблюдатель, когда происходит изменение наблюдаемого свойства. Но это также определяет, когда наблюдателю необходимо уведомлять об изменениях.

  • new: этот параметр гарантирует, что словарь изменений включает новое значение наблюдаемого свойства.
  • old: этот параметр гарантирует, что словарь изменений содержит старое значение наблюдаемого свойства.
  • initial: включив эту опцию в список опций, наблюдатель немедленно отправляет уведомление, прежде чем он будет добавлен в качестве наблюдателя.
  • previous: это вариант, который вы редко будете использовать. Этот параметр гарантирует, что наблюдатель получает уведомление до и после изменения.

context:, это более продвинутый вариант, который Вы также будете редко использовать. Он позволяет передавать дополнительные данные наблюдателю при отправке уведомления.

ViewController добавляется в качестве наблюдателя. Как он может реагировать на изменения? Просто. ViewController переопределяет функцию observValue(forKeyPath: of: change: context :), другой метод, определяемый корневым классом NSObject. Этот метод также определяет четыре параметра

forKeyPath: является #keyPath, вызвавший уведомление.

of: ссылка на объект, который он наблюдает.

change: словарь типа [NSKeyValueChangeKey : Any]?. Этот словарь может содержать несколько пар ключ-значение. Содержимое зависит от параметров, переданных addObserver(_: forKeyPath: options: context :).

context: контекст, который был передан, когда наблюдатель был добавлен ранее.

Одним из наиболее важных недостатков Key-Value Observing является то, что каждое уведомление нужно обрабатывать в методе watchValue(forKeyPath: of: change: context :). Это может стать довольно грязным делом. Однако пример, с которым мы работаем, прост.

Даже если параметр forKeyPath имеет тип String?, мы можем использовать выражение #keyPath для сравнения. Это гарантирует, что компилятор выполнит необходимую проверку во время компиляции. Если мы обнаружим, что значение свойства updatedAt конфигурации изменено, мы обновим значение метки даты. Мы могли бы вытащить значение из словаря изменений. Проблема в том, что значения в словаре имеют тип Any. Легче и безопаснее спросить у ConfigurationManager.

Запустите приложение, чтобы узнать, обновляется ли значение метки даты, если вы нажимаете кнопку Update Configuration.

WTF? Оно не работает? Сейчас исправим. В другой статье, немного позже, я расскажу Вам о таком модификаторе, как dynamic. Я уже упоминал, что Key-Value Observing возможно только благодаря Objective-C runtime и Dynamic Dispatch в частности.

Что же происходит на этом простом примере? Компилятор достаточно умён, чтобы выяснить, каким образом нужно получить доступ к свойству updatedAt класса Configuration. Это не требует Dynamic Dispatch, чтобы понять это во время выполнения. В обход Dynamic Dispatch он получает несколько наносекунд … ,но он ломает Key-Value Observing. Опять же, Key-Value Observing полагается на Dynamic Dispatch.

Как мы можем это исправить? Легко. Мы добавим префикс dynamic к свойству updatedAt. Это скажет компилятору, что свойство updatedAt всегда должно быть доступно с помощью Dynamic Dispatch.

Запустите приложение ещë раз, чтобы убедиться, что это решило проблему.

Когда приложение запускается, значение метки UILabel неверно. Мы хотим, чтобы оно отображало актуальное значение свойства updatedAt экземпляра класса Configuration, то есть текущую дату.

Вы помните метод addObserver (_: forKeyPath: options: context :), который мы обсуждали ранее? Обновите метод viewDidLoad(), как показано ниже, и запустите приложение снова.

Несмотря на то, что значение свойства updatedAt экземпляра класса Configuration ещë не изменилось, наблюдатель действительно получает уведомление. В результате ViewController обновляет значение метки даты в watchValue(forKeyPath: of: change: context :).

Ещë одна серьëзная проблема при работе с Key-Value Observing — управление памятью. Наблюдатели должны быть явно удалены, если они больше не заинтересованы в получении уведомлений для определенного #keyPath. У вас есть два варианта.

  • removeObserver(_: forKeyPath:)
  • removeObserver(_: forKeyPath: context:)

Я предполагаю, что эти методы не нуждаются в объяснении. Это показывает, насколько неуклюжим является Key-Value Observing API. Вам нужно удалить наблюдателя для каждого#keyPath. Если Вы забудете это сделать, вы получите утечку памяти или, что ещë хуже, сбой программы.

Проблемы Key-Value Observing побудили разработчиков в Facebook придумать лучшее решение. KVOController — это библиотека, которая делает работу с Key-Value Observing намного проще и безопаснее. Она использует современный API и гарантирует безопасность потоков. Вам стоит самим это проверить.

Весь проект примера тут.

)
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade