Как подклассифицировать UIScrollView и сделать свойство делегата приватным

Вот чего я хочу достичь:

Я хочу, чтобы подкласс UIScrollView имел дополнительную функциональность. Этот подкласс должен иметь возможность реагировать на прокрутку, поэтому мне нужно настроить свойство делегата на себя для получения таких событий, как:

- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { ... }

С другой стороны, другие классы также должны иметь возможность получать эти события, например, они использовали базовый класс UIScrollView.

Итак, у меня были разные идеи, как решить эту проблему, но все это не совсем меня удовлетворяет: (

Мой основной подход - использование собственного свойства делегирования следующим образом:

@interface MySubclass : UIScrollView<UIScrollViewDelegate>
@property (nonatomic, assign) id<UIScrollViewDelegate> myOwnDelegate;
@end

@implementation MySubclass
@synthesize myOwnDelegate;

- (id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.delegate = self;
    }
    return self;
}

// Example event
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // Do something custom here and after that pass the event to myDelegate
    ...
    [self.myOwnDelegate scrollViewDidEndDecelerating:(UIScrollView*)scrollView];
}
@end

Таким образом, мой подкласс может сделать что-то особенное, когда унаследованная прокрутка прокрутки заканчивается, но все же информирует внешнего делегата о событии. Это работает до сих пор. Но поскольку я хочу сделать этот подкласс доступным для других разработчиков, я хочу ограничить доступ к свойству делегата базового класса, поскольку он должен использоваться только подклассом. Я думаю, что наиболее вероятно, что другие разработчики интуитивно используют свойство делегирования базового класса, даже если я прокомментирую проблему в файле заголовка. Если кто-то изменяет свойство делегата, подкласс не будет делать то, что он должен делать, и я не могу ничего сделать, чтобы это не произошло прямо сейчас. И тот момент, когда я не знаю, как его решить.

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

@interface MySubclass : UIScrollView<UIScrollViewDelegate>
...
@property (nonatomic, assign, readonly) id<UIScrollViewDelegate>delegate;
@end

@implementation MySubclass
@property (nonatomic, assign, readwrite) id<UIScrollViewDelegate>delegate;
@end

Это приведет к предупреждению

"Attribute 'readonly' of property 'delegate' restricts attribute 'readwrite' of property inherited from 'UIScrollView'

Хорошо, плохая идея, поскольку я, очевидно, нарушаю принцип замены Лискова.

Следующая попытка → Попытка переопределить набор делегатов следующим образом:

...
- (void) setDelegate(id<UIScrollViewDelegate>)newDelegate {
    if (newDelegate != self) self.myOwnDelegate = newDelegate;
    else _delegate = newDelegate; // <--- This does not work!
}
...

Как прокомментировано, этот пример не компилируется, поскольку кажется, что _delegate ivar не был найден?! Поэтому я просмотрел файл заголовка UIScrollView и нашел это:

@package
    ...
    id           _delegate;
...

Директива @package ограничивает доступ к ivarate для доступа только к самой структуре. Поэтому, когда я хочу установить _delegate ivar, я должен использовать синтезированный сеттер. Я не вижу способа переопределить его каким-либо образом: (Но я не могу поверить, что вокруг этого не обойтись, может быть, я не вижу дерева для деревьев.

Я ценю любой намек на решение этой проблемы.


Решение:

Теперь он работает с решением @rob mayoff. Как я заметил ниже, возникла проблема с вызовом scrollViewDidScroll:. Я наконец выяснил, в чем проблема, даже я не понимаю, почему это так:/

В тот момент, когда мы устанавливаем супер-делегат:

- (id) initWithFrame:(CGRect)frame {
    ...
    _myDelegate = [[[MyPrivateDelegate alloc] init] autorelease];
    [super setDelegate:_myDelegate]; <-- Callback is invoked here
}

есть обратный вызов _myDelegate. Отладчик разбивается на

- (BOOL) respondsToSelector:(SEL)aSelector {
    return [self.userDelegate respondsToSelector:aSelector];
}

с параметром "scrollViewDidScroll:" в качестве аргумента.

Забавно, что в это время self.userDelegate еще не установлен и указывает на ноль, поэтому возвращаемое значение НЕТ! Вероятно, это приведет к тому, что методы scrollViewDidScroll: далее не будут запущены. Это похоже на предварительный запрос, если этот метод реализован, и если он не работает, этот метод вообще не будет запущен, даже если после этого мы установим свойство userDelegate. Я не знаю, почему это так, поскольку большинство других методов делегата не имеют этого предварительного запроса.

Итак, мое решение для этого - вызвать метод [super setDelegate...] в методе setDelegate PrivateDelegate, так как это место, я уверен, что мой метод userDelegate установлен.

Итак, я вернусь к этому фрагменту реализации:

MyScrollViewSubclass.m

- (void) setDelegate:(id<UIScrollViewDelegate>)delegate {
    self.internalDelegate.userDelegate = delegate;  
    super.delegate = self.internalDelegate;
}

- (id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.internalDelegate = [[[MyScrollViewPrivateDelegate alloc] init] autorelease];
        // Don't set it here anymore
    }
    return self;
}

Остальная часть кода остается нетронутой. Я все еще не очень доволен этим обходным решением, потому что он требует хотя бы один раз вызвать метод setDelegate, но он работает для моих нужд на данный момент, хотя он чувствует себя очень взломанным:/

Если у кого-то есть идеи, как улучшить это, я был бы признателен.

Спасибо @rob за ваш пример!

Ответ 1

Существует проблема с созданием MySubclass собственного делегата. Предположительно, вы не хотите запускать собственный код для всех методов UIScrollViewDelegate, но вам нужно переслать сообщения предоставленному пользователем делегату независимо от того, есть ли у вас ваша собственная реализация или нет. Таким образом, вы можете попробовать реализовать все методы делегата, причем большинство из них просто переадресовываются следующим образом:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    [self.myOwnDelegate scrollViewDidZoom:scrollView];
}

Проблема в том, что иногда новые версии iOS добавляют новые методы делегирования. Например, iOS 5.0 добавлен scrollViewWillEndDragging:withVelocity:targetContentOffset:. Таким образом, ваш подкласс scrollview не будет надежным для будущего.

Лучший способ справиться с этим - создать отдельный частный объект, который просто действует как ваш делегат scrollview и обрабатывает пересылку. Этот объект выделенного делегирования может перенаправлять каждое сообщение, которое он получает, на предоставленный пользователем делегат, поскольку он принимает только сообщения делегатов.

Вот что вы делаете. В вашем файле заголовка вам нужно только объявить интерфейс для вашего подкласса scrollview. Вам не нужно раскрывать какие-либо новые методы или свойства, поэтому он выглядит следующим образом:

MyScrollView.h

@interface MyScrollView : UIScrollView
@end

Вся настоящая работа выполняется в файле .m. Во-первых, мы определяем интерфейс для класса private delegate. Его задачей является перезванивание в MyScrollView для некоторых методов делегата и пересылка всех сообщений делегату пользователя. Поэтому мы хотим только дать ему методы, которые являются частью UIScrollViewDelegate. Мы не хотим, чтобы у него были дополнительные методы для управления ссылкой на делегат пользователя, поэтому мы просто сохраним эту ссылку как переменную экземпляра:

MyScrollView.m

@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
    id<UIScrollViewDelegate> _userDelegate;
}
@end

Далее мы реализуем MyScrollView. Он должен создать экземпляр MyScrollViewPrivateDelegate, который ему нужно иметь. Так как a UIScrollView не имеет своего делегата, нам нужна дополнительная, сильная ссылка на этот объект.

@implementation MyScrollView {
    MyScrollViewPrivateDelegate *_myDelegate;
}

- (void)initDelegate {
    _myDelegate = [[MyScrollViewPrivateDelegate alloc] init];
    [_myDelegate retain]; // remove if using ARC
    [super setDelegate:_myDelegate];
}

- (id)initWithFrame:(CGRect)frame {
    if (!(self = [super initWithFrame:frame]))
        return nil;
    [self initDelegate];
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (!(self = [super initWithCoder:aDecoder]))
        return nil;
    [self initDelegate];
    return self;
}

- (void)dealloc {
    // Omit this if using ARC
    [_myDelegate release];
    [super dealloc];
}

Нам нужно переопределить setDelegate: и delegate: для хранения и возврата ссылки на делегат пользователя:

- (void)setDelegate:(id<UIScrollViewDelegate>)delegate {
    _myDelegate->_userDelegate = delegate;
    // Scroll view delegate caches whether the delegate responds to some of the delegate
    // methods, so we need to force it to re-evaluate if the delegate responds to them
    super.delegate = nil;
    super.delegate = (id)_myDelegate;
}

- (id<UIScrollViewDelegate>)delegate {
    return _myDelegate->_userDelegate;
}

Нам также необходимо определить любые дополнительные методы, которые может понадобиться нашему частному делегату:

- (void)myScrollViewDidEndDecelerating {
    // do whatever you want here
}

@end

Теперь мы можем, наконец, определить реализацию MyScrollViewPrivateDelegate. Нам нужно явно определить каждый метод, который должен содержать наш частный пользовательский код. Метод должен выполнить наш пользовательский код и перенаправить сообщение делегату пользователя, если делегат пользователя отвечает на сообщение:

@implementation MyScrollViewPrivateDelegate

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [(MyScrollView *)scrollView myScrollViewDidEndDecelerating];
    if ([_userDelegate respondsToSelector:_cmd]) {
        [_userDelegate scrollViewDidEndDecelerating:scrollView];
    }
}

И нам нужно обрабатывать все другие методы UIScrollViewDelegate, для которых у нас нет настраиваемого кода, и все те сообщения, которые будут добавлены в будущих версиях iOS. Мы должны реализовать два метода, чтобы это произошло:

- (BOOL)respondsToSelector:(SEL)selector {
    return [_userDelegate respondsToSelector:selector] || [super respondsToSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    // This should only ever be called from `UIScrollView`, after it has verified
    // that `_userDelegate` responds to the selector by sending me
    // `respondsToSelector:`.  So I don't need to check again here.
    [invocation invokeWithTarget:_userDelegate];
}

@end

Ответ 2

Спасибо @robmayoff Я инкапсулирован как более общий делегат-перехватчик: Наличие оригинального класса MessageInterceptor:

MessageInterceptor.h

@interface MessageInterceptor : NSObject

@property (nonatomic, assign) id receiver;
@property (nonatomic, assign) id middleMan;

@end

MessageInterceptor.m

@implementation MessageInterceptor

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.middleMan respondsToSelector:aSelector]) { return    self.middleMan; }
    if ([self.receiver respondsToSelector:aSelector]) { return self.receiver; }
    return [super forwardingTargetForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([self.middleMan respondsToSelector:aSelector]) { return YES; }
    if ([self.receiver respondsToSelector:aSelector]) { return YES; }
    return [super respondsToSelector:aSelector];
}

@end

Он используется в вашем общем классе делегатов:

GenericDelegate.h

@interface GenericDelegate : NSObject

@property (nonatomic, strong) MessageInterceptor * delegate_interceptor;

- (id)initWithDelegate:(id)delegate;

@end

GenericDelegate.m

@implementation GenericDelegate

- (id)initWithDelegate:(id)delegate {
    self = [super init];
    if (self) {
        self.delegate_interceptor = [[MessageInterceptor alloc] init];
        [self.delegate_interceptor setMiddleMan:self];
        [self.delegate_interceptor setReceiver:delegate];
    }
    return self;
}

// delegate methods I wanna override:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // 1. your custom code goes here
    NSLog(@"Intercepting scrollViewDidScroll: %f %f", scrollView.contentOffset.x, scrollView.contentOffset.y);

    // 2. forward to the delegate as usual
    if ([self.delegate_interceptor.receiver respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.delegate_interceptor.receiver scrollViewDidScroll:scrollView];
    }
 }
//other delegate functions you want to intercept
...

Поэтому я могу перехватить любого делегата, который мне нужен, на любом UITableView, UICollectionView, UIScrollView...:

@property (strong, nonatomic) GenericDelegate *genericDelegate;
@property (nonatomic, strong) UICollectionView* collectionView;

//intercepting delegate in order to add separator line functionality on this scrollView
self.genericDelegate = [[GenericDelegate alloc]initWithDelegate:self];
self.collectionView.delegate = (id)self.genericDelegate.delegate_interceptor;

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"Original scrollViewDidScroll: %f %f", scrollView.contentOffset.x, scrollView.contentOffset.y);
}

В этом случае функция UICollectionViewDelegate scrollViewDidScroll: будет выполняться в нашем GenericDelegate (с любым кодом, который мы хотим добавить) и в процессе реализации нашего собственного класса

Вот мои 5 центов, благодаря @robmayoff и @jhabbott предыдущий ответ

Ответ 3

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

@interface EnhancedTableViewController : UITableViewController <UITableViewDelegate>

Там вы переопределите некоторый метод делегата и определите свою абстрактную функцию

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // code before interception
    [self bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:indexPath];
    // code after interception
}

- (void)bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {};

Теперь вам нужно только подклассифицировать EnhancedTableViewController и использовать абстрактные функции вместо делегирования. Вот так:

@interface MyTableViewController : EnhancedTableViewController ...

- (void)bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //overriden implementation with pre/post actions
};

Сообщите мне, если что-то не так.

Ответ 4

Не подклассифицируйте его, инкапсулируйте его;)

Создайте новый подкласс UIView - ваш заголовочный файл будет выглядеть так:

@interface MySubclass : UIView 
@end

И просто UIScrollView как подвью - ваш .m файл будет выглядеть так:

@interface MySubclass () <UIScrollViewDelegate>

@property (nonatomic, strong, readonly) UIScrollView *scrollView;

@end


@implementation MySubClass

@synthesize scrollView = _scrollView;

- (UIScrollView *)scrollView {
    if (nil == _scrollView) {
        _scrollView = [UIScrollView alloc] initWithFrame:self.bounds];
        _scrollView.delegate = self;
        [self addSubView:_scrollView];
    }
    return _scrollView;
}

...
your code here
...

@end

Внутри вашего подкласса всегда используйте self.scrollView, и он будет создавать прокрутку в первый раз, когда вы его попросите.

Это позволяет полностью скрывать вид прокрутки от любого, кто использует ваш MySubClass, - если вам нужно изменить способ работы за кулисами (т.е. изменить вид прокрутки на веб-представление), было бы очень легко do:)

Это также означает, что никто не может изменить поведение прокрутки:)

PS Я предположил, что ARC - измените strong на retain и добавьте dealloc при необходимости:)


ИЗМЕНИТЬ

Если вы хотите, чтобы ваш класс вел себя точно как UIScrollView, вы можете попробовать это, добавив этот метод (взятый из эти документы и непроверенный!)

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([self.scrollView respondsToSelector:anInvocation.selector])
        [anInvocation invokeWithTarget:self.scrollView];
    else
        [super forwardInvocation:anInvocation];
}