Здравствуйте!
— Не хотите ли войти
Tags Switch

Objective-C биндинги

2 декабря 2010
Уровень сложности: для разработчиков с небольшим опытом

Аннотация

Наверное, наиболее привлекательной возможностью в 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.

Комментарии к документу
Зарегистрируйтесь или войдите, чтобы оставить комментарий.

Не знаю к месту ли, а в чем разница между setValue: forKey; и setObject: forKey: ?

arkichek

В типах параметров (ключей). :) Плюс, реакция на nil вроде бы немного отличается.

alex

Спасибо за статью — когда то читал, а теперь пригодилось :) Зашел снова пересмотреть ее.

Польза например от unionOfObjects есть! У меня по крайне мере — например есть массив с огромным кол-вом элементов и нужно получить у каждого объекта данные определенных полей — так вот же одной строчкой и решение как гвоорится :)

зы: у вас и в офф доке очепятка во всех строках типа: NSArray *payees=[transactions valueForKeyPath"@distinctUnionOfObjects.payee"];

после valueForKeyPath нужно :@ еще добавлять

olgerd
© 2009-2012, ООО «Инру»
Вход
Имя пользователя:
Пароль:
Или…
Twi
Отмена
Войти
Восстановить забытый пароль…