How to subclass UIScrollView and make the delegate property private How to subclass UIScrollView and make the delegate property private ios ios

How to subclass UIScrollView and make the delegate property private


There is a problem with making MySubclass its own delegate. Presumably you don't want to run custom code for all of the UIScrollViewDelegate methods, but you have to forward the messages to the user-provided delegate whether you have your own implementation or not. So you could try to implement all of the delegate methods, with most of them just forwarding like this:

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

The problem here is that sometimes new versions of iOS add new delegate methods. For example, iOS 5.0 added scrollViewWillEndDragging:withVelocity:targetContentOffset:. So your scrollview subclass won't be future-proof.

The best way to handle this is to create a separate, private object that just acts as your scrollview's delegate, and handles forwarding. This dedicated-delegate object can forward every message it receives to the user-provided delegate, because it only receives delegate messages.

Here's what you do. In your header file, you only need to declare the interface for your scrollview subclass. You don't need to expose any new methods or properties, so it just looks like this:

MyScrollView.h

@interface MyScrollView : UIScrollView@end

All the real work is done in the .m file. First, we define the interface for the private delegate class. Its job is to call back into MyScrollView for some of the delegate methods, and to forward all messages to the user's delegate. So we only want to give it methods that are part of UIScrollViewDelegate. We don't want it to have extra methods for managing a reference to the user's delegate, so we'll just keep that reference as an instance variable:

MyScrollView.m

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

Next we'll implement MyScrollView. It needs to create an instance of MyScrollViewPrivateDelegate, which it needs to own. Since a UIScrollView doesn't own its delegate, we need an extra, strong reference to this object.

@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];}

We need to override setDelegate: and delegate: to store and return a reference to the user's 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;}

We also need to define any extra methods that our private delegate might need to use:

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

Now we can finally define the implementation of MyScrollViewPrivateDelegate. We need to explicitly define each method that should contain our private custom code. The method needs to execute our custom code, and forward the message to the user's delegate, if the user's delegate responds to the message:

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

And we need to handle all of the other UIScrollViewDelegate methods that we don't have custom code for, and all of those messages that will be added in future versions of iOS. We have to implement two methods to make that happen:

- (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


Thanks @robmayoff I encapsulated to be a more generic delegate interceptor:Having original MessageInterceptor class:

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

It is used in your generic delegate class:

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...

So I can intercept any delegate I need to on any UITableView, UICollectionView, UIScrollView... :

@property (strong, nonatomic) GenericDelegate *genericDelegate;@property (nonatomic, strong) UICollectionView* collectionView;//intercepting delegate in order to add separator line functionality on this scrollViewself.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);}

In this case UICollectionViewDelegate scrollViewDidScroll: function will be executed on our GenericDelegate (with any code we want to add) and on in the implementation of our own class

Thats my 5 cents, thanks to @robmayoff and @jhabbott previous answer


Another option is to subclass and use the power of abstract functions.For instance, you create

@interface EnhancedTableViewController : UITableViewController <UITableViewDelegate>

There you override some delegate method and define your abstract function

- (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 {};

Now all you need to do is to subclass your EnhancedTableViewController and use your abstract functions instead of delegate ones. Like this:

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

Let me know if there is anything wrong here.