Pull to refresh
0
e-legion
Делаем приложения, которыми пользуются миллионы

Multiple Delegate

Reading time 4 min
Views 10K
В 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 самому

  • .
Tags:
Hubs:
+18
Comments 6
Comments Comments 6

Articles

Information

Website
www.e-legion.ru
Registered
Founded
Employees
101–200 employees
Location
Россия