Уже много копий было сломанно о тему «обработка событий в Objective-C», о делегировании событий (к примеру, viewWillAppear:(BOOL)animated ), о том как это не удобно, когда надо слушать их одновременно в разных местах программы.
Я хочу предложить Вам свою реализацию шаблона Observer, который использует мощь C++0x и позволяет объявлять сигналы с жёстко типизированным списком параметров, например, вот так:
new TLSignal<NSString *, BOOL>(self);
Т.к. мои знания С++ довольно таки скудны, то буду признателен любым советам по улучшению данного кода.
Заинтересовавшихся прошу под кат.
Суть проблемы
Часто встречаются ситуации, когда необходимо уведомить другие объекты о каком-либо событии (свойство изменилось, смена состояния и так далее), при этом необходимо иметь возможность соблюдать соответствие «One-to-many», чтобы событие одновременно могли получить несколько слушателей. По этой причине вариант с делегатами сразу отпадает.
Какие варианты предлагают нам встроенные средства Objective-C?
- Notification Center — безусловно, у NC есть своя область применения, но он больше подходит для глобальных событий внутри всей программы, так же идентификация событий идёт по строковому идентификатору, что не есть хорошо. Ну и производительность такого решения оставляет желать лучшего, как и API.
- Key-Value Observing(KVO) — частный случай события, когда какое-то свойство объекта меняется. Позволяет делать чудесные вещи вроде Bindings, но API сводит все его прелести на нет.
Observer pattern
Лучше статьи на википедии думаю у меня рассказать не получится, поэтому я лишь уточню детали:
- Слушателями наших событий будут выступать обычные Objective-C блоки.
- Объект, рассылающий события с помощью моих сигналов, сам ответственнен за их создание и уничтожение.
- Типы и количество параметров метода, выполняемого при оповещении, будут задаваться через шаблоны C++ и проверяться на этапе компиляции.
Итак, объявление сигнала выглядит вот так:
new TLSignal<NSString *, BOOL>(self);
Что означает, что:
- Мы создали сигнал, который принимает 2 параметра: строку (NSString) и булеву (BOOL)
- Target-ом для нашего сигнала будет self, т.е. объект, в котором этот сигнал создаётся.
При этом блок, слушающий данное событие, будет выглядеть вот так:
auto observerBlock = ^(id target, NSString *stringParam, BOOL boolParam)
{
NSLog(@"%@ %@ %d", target, stringParam, boolParam);
};
Обратите внимание на 1ый параметр id target, он передаётся всегда и равен «держателю» данного сигнала.
Пример использования
Код без примера использования — не код, поэтому хочу привести маленький пример программы, на примере которой хотел бы продемонстрировать, в чем собственно преимущество сигналов и как их можно использовать.
ExampleViewController.h
#import <UIKit/UIKit.h>
#import "Signals.h"
@interface ExampleViewController : UIViewController
@property (nonatomic, readonly) TLSignal<UIView *> *viewDidLoadSignal;
@end
Здесь всё просто, объявляем сигнал с идентификатором viewDidLoadSignal, который будет передавать экземляр UIView слушателям.
ExampleViewController.mm
#import "ExampleViewController.h"
@implementation ExampleViewController
@synthesize viewDidLoadSignal = _viewDidLoadSignal;
-(TLSignal<UIView *> *)viewDidLoadSignal
{
if(!_viewDidLoadSignal)
{
// Лениво создаём экземпляр сигнала и передаём ему в конструктор ссылку на себя (источник события)
_viewDidLoadSignal = new TLSignal<UIView *>(self);
}
return _viewDidLoadSignal;
}
-(void)viewDidLoad
{
[super viewDidLoad];
// Рассылаем событие слушателям, передав в них наш UIView
self.viewDidLoadSignal->notify(self.view);
}
@end
AppDelegate.mm
#import "AppDelegate.h"
#import "ExampleViewController.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options
{
UIWindow window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.viewController = [[ExampleViewController alloc] initWithNibName:@"ViewController" bundle:nil];
// Добавим сигналу слушателя, который по событию поменяет цвет переданного в него UIView
self.viewController.viewDidLoadSignal->addObserver(^(id target, UIView *targetView)
{
targetView.backgroundColor = [UIColor blackColor];
});
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
Обратите внимание, что расширение файлов с кодом, в котором используются сигналы, должно быть .mm а не .m, иначе он будет воспринят как обычный Objective-C код и не поймёт нашей плюсовой магии!
Заключение
Я искренне надеюсь, что в Objective-C (например, 2.5 или 3.0) появится что-то подобное на уровне стандарта, а пока это не произошло, предлагаю Вам использовать мою библиотеку (она крайне лёгкая в использовании, код так же понятен даже с небольшими знаниями в C++, коими я и обладаю:)):
TLSignals на GitHub
На днях она появится в Cocoapods, а пока это не произошло, Вы можете просто скопировать файл TLSignals.h к себе в проект и начать работать с ним.
Код частично покрыт тестами, чтение которых поможет Вам лучше понять, как оно работает:
TLSignalsTests.mm на GitHub
Буду благодарен за дополнения и улучшения, а так же тыкание носом меня в слабые моменты со стороны C++ и вообще, потому что все мы только учимся;)
При обнаружении опечаток или грамматических ошибок просьба писать о них личным сообщением, дабы не засорять этим комментарии.
Спасибо за внимание!
Ссылки по теме
- github.com/bsideup/TLSignals — Сама библиотека, если её так можно назвать.
- en.wikipedia.org/wiki/Observer_pattern — Описание паттерна Observer.
- en.wikipedia.org/wiki/Variadic_Templates — Variadic Templates. Без этого не получилось бы сделать разное количество аргументов в зависимости от типа сигнала.
- habrahabr.ru/post/101430/ — о них же, но в формате родного Хабра.
Автор: bsideup