Objective-C биндинги

Наверное, наиболее привлекательной возможностью в Objective-C я бы назвал биндинги. И если про то, как связать данные с графическое представлением при помощи InterfaceBuilder написано очень и очень много (даже на русском ), то вот о том, как и почему вся эта магия работает можно найти разве что на сайте Apple.
Итак, вся технология биндингов базируется на двух паттернах: Key-Value Coding и Key-Value Observing. Думаю, для начала стоит написать именно про эти паттерны, а потом перейти к самим биндингам.

Итак, Key-Value Coding позволяет обратиться к данным класса по имени, включая вложенную обращения с учетом вложенной иерархии. Для начала небольшой пример иллюстрирующий, о чем же идет речь.

Пусть будет класс MyClass, следующего вида:

@interface MyClass 
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end

Обычно, доступ к данным этого класса реализуется как-то так:

MyClass *myInstance = [[MyClass alloc] init]; 
NSString *string = myInstance.stringProperty;
myInstance.integerProperty = 2;

MyClass *anotherInstance = [[MyClass alloc] init];
anotherInstance.integerProperty = 5;
myInstance. linkedInstance = anotherInstance;
myInstance. linkedInstance. integerProperty = 10;

В то же время, при использовании Key-Value Coding можно написать так:

MyClass *anotherInstance = [[MyClass alloc] init]; 
[anotherInstance setValue:5 forKey:@"integerProperty"];
[myInstance setValue:anotherInstance forKey:@"linkedInstance"];
[myInstance setValue:[NSNumber numberWithInt:10]
forKeyPath:@"linkedInstance.integerProperty"];

Для работы KVO паттерна необходимо чтобы класс соответствовал следующим условиям:
– Должны присутствовать методы –, -is, либо должна существовать переменная класса <key>;
– Для изменяемых значений так же необходимо наличи метода -set:;
– Метод -set: не должен проверять устанавливаемое значение.
– Если проверка устанавливаемого значения все-же необходима, в классе должен присутствовать метод -validate:error:.
На мой взгляд, самый простой способ поддержать эти требования – это создать это использовать свойства.

Для получения значений атрибутов используется метод valueForKey:. В случае если необходимо запросить значение с использованием пути, необходимо воспользоваться методом valueForKeyPath:. В случае, если запрашиваемый ключ отсутствует возбуждается исключение NSUndefinedKeyException.

Задание значения происходит полностью по аналогии с получением значения. Есть методо setValue:forKey:, устанавливающий значение указанного атрибута и setValue:forKeyPath: если необходимо задать путь. При отсутствии заданного ключа, так же будет возбуждено исключение NSUndefinedKeyException.

А вот функционал операций с коллекциями меня приятно удивил. В Objective-C есть операторы коллекций (Collection Operators), благодаря которым можно манипулировать с коллекциями по подобию SQL-запросов. Само собой, возможности очень ограниченные, тем не менее полезные.
Операторы коллекций записываются следующим образом:
путьВКоллекции.@операторКоллекции.путьКСвойству

Операторы коллекций делятся на 3 группы: простые операторы, операторы объектов и операторы массивов.
К простым операторам относятся @avg, @count, @max, @min, @sum. Их использование выгляди следующим образом. Допустим есть массив объектов Transaction с полями payee, amount и date. Тогда для получения среднего значения по полю amount можно написать такой запрос:

NSNumber *transactionAverage=[transactions valueForKeyPath”@avg.amount”];

А вот с операторы объектов радуют еще больше, например для получения выборки уникальных значений поля payee можно воспользоваться вот таким запросом:

NSArray *payees=[transactions valueForKeyPath"@distinctUnionOfObjects.payee"];

Кроме того, есть еще один оператор объектов – @unionOfObjects.

Операторы массивов, вобщем-то тоже самое что и операторы объектов, только применяются к нескольким массивам, содержащим однотипные объекты, одновременно. Какой-то практической пользы я в них не нашел.

Разве что отсутствие возможности добавить собственный оператор для работы с коллекциями меня огорчило. Может я и не прав, но никакой информации о том, как добавить еще один оператор я не нашл.

Вторая составляющая механизма биндингов это Key-Value Observing (KVO). KVO позволяет получать уведомления об изменении заданных свойств других объектов. Проще говоря, это callback механизм, работающий по заданному стандарту.

Для регистрации observer используется сообщение addObserver:forKeyPath:options:context:. Например для того чтобы получать сообщения об изменениях свойства openingBalance объекта account необходимо послать следующее сообщение:

[account addObserver:self
forKeyPath:@"openingBalance" options:(NSKeyValueObservingOptionNew) context:NULL];

В результате чего, при изменении значения openingBalance объекта account будет вызван метод observeValueForKeyPath:ofObject:change:context: для self. В качестве context можно передать любой C-указатель.

Нотификации об изменении свойства приходит в метод observeValueForKeyPath:ofObject:change:context:. Нужно не забывать о том, что полученное сообщение должно быть проброшено дальше:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
{
// to-do smth.
[super observeValueForKeyPath:keyPath
ofObject:object change:change
context:context];
}

Если получать нотификации больше не требуется, необходимо вспользоваться методом removeObserver:forKeyPath:.

Несмотря на то, что в большинстве случаев вполне достаточно автоматического извещения об изменениях свойств, уведомления можно сделать и в ручную. Лично мне эта возможность пригодилась только однажды – для оптимизации скорости работы GUI. Дело в том, что при отображении массива динамически изменяемых данных, интерфейс начинает перерисовываться при изменении каждого из элементов массива. И, как следствие, вместо одного обновления всех элементов разом, происходило N обновлений, что требовало дополнительных ресурсов.
К счастью, при помощи метода willChangeValueForKey: можно сообщить о том, что началось изменение значений для определенного ключа и закончить его вызовом метода didChangeValueForKey:.

Честно сказать, после того как становится понятно с KVO и KVC говорить про биндинги уже особо и нечего. Для создания биндинга используется сообщение bind:toObject:withKeyPath:options:, которое говорит получателю поддерживать заданный атрибут в синхронном состоянии. Например сообщение [joystick bind:@”angle” toObject:GraphicController withKeyPath:@”selection.shadowAngle” options:options]; можно интерпретировать следующим образом:
– bind: @”angle” если значение ассоциируемое с angle изменится,
– toObject: GraphicController то необходимо сообщиться заданному объекту GraphicController
– withKeyPath: @”selection.shadowAngle” что его значение selection.shadowAngle было изменено
– options: options используя трансформер options

Так же стоит заметить, что в iOS метод bind:toObject:withKeyPath:options: вообще не доступен. И при необходимости реализуется самостоятельно.

Для упрощения работы с биндигами, существует ряд стандартных контроллеров, облегчающих работу с массивами и древовидными структурами данных. Это NSArrayController, NSTreeController и NSDictionaryController. Кроме того, существуют контроллеры для управления видами NSViewController, объектами NSObjectController и пользовательскими настройками UserDefaultsController.

Leave a Reply