Multiple Delegate

в 13:15, , рубрики: Cocoa, iOS, ios development, ios programming, iOS разработка, objective-c, Блог компании e-Legion Ltd., разработка под iOS, метки: , , , , ,

В 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, вызывая у объекта - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector. Если метод вместо NSInvocation вернет nil, то рантайм вызовет у объекта -(void)doesNotRecognizeSelector:(SEL)aSelector, т.е. произойдет крэш.

5. После того, как invocation создан, происходит вызов -(void)forwardInvocation:(NSInvocation *)anInvocation.
Переопределив этот метод, можно передать invocation другому объекту, например так:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

NSProxy

Из описанного выше следует, что для реализации Method Forwarding достаточно у наследника NSObject переопределить -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector и -(void)forwardInvocation:(NSInvocation *)anInvocation:.

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

Реализуем Multiple Delegate

Итак, используя знания о том, как работает Message Forwarding, реализуем наш PTCMultipleDelegate.

Учитывая требования к прокси, получаем для него следующий интерфейс

@interface PTCMultipleDelegate : NSProxy

@property (nonatomic, weak, readonly) id mainDelegate;
@property (nonatomic, strong, readonly) NSPointerArray *delegates;
@property (nonatomic, assign, readonly) BOOL mainDelegateIsDelegator;

+ (instancetype)newProxyForMainDelegate:(id)mainDelegate
                              delegates:(NSArray *)delegates
                mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator;

@end

Как уже было написано, если делегируемая функция возвращает значение, то надо как-то определить, результат вызова какого делегата считать за возвращаемое значение. Для этой цели служит mainDelegate. Если mainDelegate отвечает на селектор, то в качестве результата будет возвращено значение из mainDelegate, если нет, то значение последнего делегата из delegates.

Выглядит это так:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    BOOL isMethodReturnSomething = (![[NSString stringWithCString:invocation.methodSignature.methodReturnType
                                                         encoding:NSUTF8StringEncoding]
                                      isEqualToString:@"v"]);
    
	if ([self shouldCallMainDelegateForSelector:invocation.selector]) {
		[invocation invokeWithTarget:self.mainDelegate];
    }
	
	NSInvocation *targetInvocation = invocation;
	if (isMethodReturnSomething) {
		targetInvocation = [invocation copy];
	}
	
	for (id delegate in self.delegates) {
		if ([delegate respondsToSelector:invocation.selector]) {
			[targetInvocation invokeWithTarget:delegate];
        }
    }
}

Метод copy для NSInvocation реализован в категории следующим образом:

@implementation NSInvocation (PTCUtils)

- (instancetype)copy
{
	NSInvocation *copy = [NSInvocation invocationWithMethodSignature:[self methodSignature]];
	NSUInteger argCount = [[self methodSignature] numberOfArguments];
	
	for (int i = 0; i < argCount; i++)
	{
		char buffer[sizeof(intmax_t)];
		[self getArgument:(void *)&buffer atIndex:i];
		[copy setArgument:(void *)&buffer atIndex:i];
	}
    
	return copy;
}

@end

У UITextField есть приватный метод -(void)keyboardInputChangedSelection:(id)selection
Который внутри делает примерно следующее:

- (void)keyboardInputChangedSelection:(id)selection
{
    if ([self isEditing]) {
        if ([self.delegate respondsToSelector:NSSelectorFromString(@"keyboardInputChangedSelection:")]) {
            if (self.delegate != self) {
                [self.delegate keyboardInputChangedSelection:selection];
            }
        }
    }
}

Таким образом, если наш наследник UITextField сделает через прокси делегатом самого себя, то приложение может попасть в бесконечный цикл. Чтобы этого избежать, используется свойство mainDelegateIsDelegator. Если его значение YES, то метод из mainDelegate будет вызван только в том случае, если он реализован в сабклассе.

Для проверки того, что метод реализован в сабклассе, а не в базовом классе, используется следующая функция:

BOOL SelectorOverridenInSubclass(Class subclass, SEL aSelecor)
{
    if (method_getImplementation(class_getInstanceMethod(subclass, aSelecor)) !=
        method_getImplementation(class_getInstanceMethod(class_getSuperclass(subclass), aSelecor))) {
        return YES;
    } else {
        return NO;
    }
}

Итак, все вместе:

@interface PTCMultipleDelegate ()

@property (nonatomic, weak) id mainDelegate;
@property (nonatomic, strong) NSPointerArray *delegates;

@end

@implementation PTCMultipleDelegate

+ (instancetype)newProxyForMainDelegate:(id)mainDelegate
                              delegates:(NSArray *)delegates
                mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator;
{
    return [[self alloc] initProxyMainDelegate:mainDelegate
                                     delegates:delegates
                       mainDelegateIsDelegator:mainDelegateIsDelegator];
}

- (instancetype)initProxyMainDelegate:(id)mainDelegate
                            delegates:(NSArray *)delegates
              mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator
{
    _mainDelegateIsDelegator = mainDelegateIsDelegator;
    _mainDelegate = mainDelegate;
    _delegates = [NSPointerArray weakObjectsPointerArray];
    [delegates enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [_delegates addPointer:(__bridge void *)obj];
    }];
    return self;
}

- (BOOL)shouldCallMainDelegateForSelector:(SEL)aSelector
{
    if ([self.mainDelegate respondsToSelector:aSelector]) {
        if (self.mainDelegateIsDelegator) {
            if (SelectorOverridenInSubclass([self.mainDelegate class], aSelector)) {
                return YES;
            }
        } else {
            return YES;
        }
    }
    
    return NO;
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ([self shouldCallMainDelegateForSelector:aSelector]) {
        return YES;
    }

    for (id delegate in self.delegates) {
		if ([delegate respondsToSelector:aSelector]) {
			return YES;
        }
    }
    
    return NO;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [[self.mainDelegate class] instanceMethodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    BOOL isMethodReturnSomething = (![[NSString stringWithCString:invocation.methodSignature.methodReturnType
                                                         encoding:NSUTF8StringEncoding]
                                      isEqualToString:@"v"]);
    
	if ([self shouldCallMainDelegateForSelector:invocation.selector]) {
		[invocation invokeWithTarget:self.mainDelegate];
    }
	
	NSInvocation *targetInvocation = invocation;
	if (isMethodReturnSomething) {
		targetInvocation = [invocation copy];
	}
	
	for (id delegate in self.delegates) {
		if ([delegate respondsToSelector:invocation.selector]) {
			[targetInvocation invokeWithTarget:delegate];
        }
    }
}

@end

Сэмпл

Пример доступен на GitHub.

Автор: Fanruten

Источник

Поделиться

* - обязательные к заполнению поля