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:of
Object:change:context
: для self. В качестве context можно передать любой C-указатель.
Нотификации об изменении свойства приходит в метод observeValueForKeyPath:of
Object: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.
Не знаю к месту ли, а в чем разница между setValue: forKey; и setObject: forKey: ?
В типах параметров (ключей). :) Плюс, реакция на nil вроде бы немного отличается.
Спасибо за статью — когда то читал, а теперь пригодилось :) Зашел снова пересмотреть ее.
Польза например от unionOfObjects есть! У меня по крайне мере — например есть массив с огромным кол-вом элементов и нужно получить у каждого объекта данные определенных полей — так вот же одной строчкой и решение как гвоорится :)
зы: у вас и в офф доке очепятка во всех строках типа: NSArray *payees=[transactions valueForKeyPath"@distinctUnionOfObjects.payee"];
после valueForKeyPath нужно :@ еще добавлять