Лидер мобильной разработки в России
121,93
рейтинг
30 июня 2014 в 17:15

Разработка → Multiple Delegate

В Cocoa очень популярен паттерн делегирование. Стандартный способ реализации этого паттерна — добавление к делегатору weak свойства, которое хранит ссылку на делегат.

У делегирования много различных применений. Например, реализация какого-то поведения в другом классе без наследования. Еще делегирование используется как способ передачи уведомлений. Например, UITextField вызывает у делегата метод textFieldDidEndEditing:, который информирует его о том, что редактирование закончено, и т.д.

А теперь представьте задачу: надо сделать так, чтобы делегатор посылал сообщения не одному делегату, а нескольким, причем делегирование реализовано стандартным методом через свойство.

Пример


Пример немного притянутый, но все же.

Нужно сделать кастомный UITextField, который будет проверять введенный в него текст, и если текст невалидный, то контрол будет менять цвет. Плюс надо сделать так, чтобы пользователь мог ввести только заданное число символов.

Т.е хотим что-то вроде этого:
@protocol PTCTextValidator <NSObject>

- (BOOL)textIsValid:(NSString *)text;
- (BOOL)textLengthIsValid:(NSString *)text;

@end

@interface PTCVerifiableTextField : UITextField

@property (nonatomic, weak) IBOutlet id<PTCTextValidator> validator;
@property (nonatomic, strong) UIColor *validTextColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *invalidTextColor UI_APPEARANCE_SELECTOR;

@property (nonatomic, readonly) BOOL isValid;

@end

И тут возникает проблема. Чтобы PTCVerifiableTextField реализовал кастомное поведение, нужно чтобы он был делегатом своего суперкласса (UITextField). Но если так сделать, то нельзя будет трогать свойство delegate извне.
Т.е. нижеприведенный код поломает внутреннюю логику PTCVerifiableTextField
PTCVerifiableTextField *textField = [PTCVerifiableTextField alloc] initWIthFrame:CGrectMake(0, 0, 100 20)];  
textField.delegate = self;  
[self.view addSubview:textField];  

Таким образом, получаем задачу: сделать так, чтобы свойству
@property(nonatomic, assign) id<UITextFieldDelegate> delegate  
можно было присвоить несколько объектов.

Решение


Решение напрашивается само собой. Надо сделать контейнер, который будет хранить несколько делегатов и сообщения, которые будут приходить не в него, а в объекты, которые хранятся в контейнере.
Т.е нужен контейнер, проксирующий запросы к хранящимся в нем элементам.

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

Итак, перед тем, как что-то проксировать, надо разобраться, что такое Message Forwarding и NSProxy.

Message Forwarding


Objective-C работает с сообщениями. Мы не вызываем метод на объекте. Вместо этого, мы шлем ему сообщение. Таким образом, под Message Forwarding понимается редирект сообщения другому объекту, т.е. его проксирование.

Важно отметить, что отправка объекту сообщения, на которое он не отвечает, дает ошибку. Однако перед тем, как ошибка будет сгенерирована, рантайм даст объекту еще один шанс, чтобы обработать сообщение.

Давайте рассмотрим, что происходит при отправке объекту сообщения.

1. Если объект реализует метод, т.е можно получить IMP (например, при помощи method_getImplementation(class_getInstanceMethod(subclass, aSelecor))), то рантайм вызывает метод. В противном случае, идем дальше.

2. Вызывается +(BOOL)resolveInstanceMethod:(SEL)aSEL или +(BOOL)resolveClassMethod:(SEL)name, если шлем сообщение классу. Этот метод дает возможность добавить нужный селектор динамически. Если возвращается YES, то рантайм сново пытается получить IMP и вызвать метод. В противном случае, идем дальше.

Еще данный метод вызывается при +(BOOL)respondsToSelector:(SEL)aSelector и +(BOOL)instancesRespondToSelector:(SEL)aSelector, если селектор не реализован. Причем, данный метод вызывается только один раз для каждого селектора, второго шанса добавить метод не будет!

Пример динамического добавления метода:
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically))
    {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSel];
}


3. Выполняется так называемый Fast Forwarding. А именно, вызывается метод -(id)forwardingTargetForSelector:(SEL)aSelector
Этот метод возвращает объект, который надо использовать вместо текущего. В общем-то, очень удобная штука для имитации множественного наследования. Fast он, потому что на данном этапе можно сделать форвардинг без создания NSInvoacation.

Для возвращенного этим методом объекта будут повторены все шаги. Согласно документации, если вернуть self, то будет бесконечный цикл. На практике, бесконечного цикла не возникает: видимо, в рантайм внесли поправки.

4. Два предыдущих шага являются оптимизацией форвардинга. После них рантайм создает NSInvocation.
Создание NSInvocation рантаймом выглядит примерно так:
NSMethodSignature *sig = ...
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setSelector:selector];

Т.е для создания NSInvocation, рантайму надо получить сигнатуру метода (NSMethodSignature). Поэтому у объекта вызывается - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector. Если метод вместо NSMethodSignature вернет nil, то рантайм вызовет у объекта -(void)doesNotRecognizeSelector:(SEL)aSelector, т.е. произойдет крэш.

Создать NSMethodSignature можно следующими способами:
  • использовать метод +(NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
    Заметьте, если класс всего лишь заявляет, что реализует протокол (@interface MyClass : NSObject ), то этот методу уже вернет не nil, а сигнатуру.

    использовать метод -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    Внутри вызывает [[self class] instanceMethodSignatureForSelector:...]

    использовать метод +(NSMethodSignature *)signatureWithObjCTypes:(const char *)types принадлежащий классу NSMethodSignature и собрать NSMethodSignature самому

  • .
Автор: @Fanruten
e-Legion Ltd.
рейтинг 121,93
Лидер мобильной разработки в России

Комментарии (6)

  • –2
    Отличный разбор проблемы! Заодно стало яснее процедура обработки сообщений.

    ИМХО практика использования делегатов конечно убога, по сравнению с событиями в том же C#, но деваться некуда, пока лишь небольшая часть API перешла на использования блоков в качестве колбэков.
  • +4
    Вам не кажется, что было бы гораздо проще и выразительнее создать UIView, которая бы агрегировала UITextField, и перенаправлять все вызовы делегата ручками?
    Message forwarding и objc-рантайм — это, конечно, очень мощные инструменты, но читать, поддерживать и отлаживать такой код потом становится сложно, даже человеку, его написавшему. Если же вам в действительности нужно множественное делегирование, то стоит его реализовать более выразительно и предсказуемо.
    • 0
      Мне показалось, что Multiple Delegate будет интересен в качестве примера.

      А так, в тексте прям предложение есть
      Пример немного притянутый, но все же.
  • +3
    mainDelegate объявлен как weak. Что будет, если он умрет раньше, чем прокси? Правильно: doesNotRecognizeSelector: (uncaught exception).

    Это стандартные грабли, на которые натыкаешься при играх с NSProxy и делегированием. Обойти которые можно через предварительное создание кэша сигнатур возможных селекторов, чтобы даже если все делегаты умерли, можно было успешно завершить форвардинг без крэша. А поскольку создавать кэш для вообще всех возможных сигнатур нелепо, в инициализатор прокси обычно передают протокол, из которого (и рекурсивно из всех родительских протоколов) выдергиваются сигнатуры для обязательных и опциональных методов.

    Я делал практически то же самое в своей библиотечке FastElegantDelegation. Также можно посмотреть пример с сингл-прокси в библиотечке PSTDelegateProxy, где к слову изначально была та же проблема.
    • +1
      Большое спасибо за комментарий. Немного дополнил статью.
      • +1
        А если главный делегат не реализует опциональный метод протокола, а один из дополнительных — да?
        Тут граблей много. :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка