Here is what I want to achieve:
I want to subclass an UIScrollView to have additional functionality. This subclass should be able to react on scrolling, so i have to set the delegate property to self to receive events like:
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { ... }
On the other hand, other classes should still be able to receive these events too, like they were using the base UIScrollView class.
So I had different ideas how to solve that problem, but all of these are not entirely satisfying me :(
My main approach is..using an own delegate property like this:
@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
In that way my subclass can do something special when the inherited scrollview ends scrolling, but still informs the external delegate of the event. That works so far. But as I want to make this subclass available to other developers, I want to restrict access to the base class delegate property, as it should only be used by the subclass. I think it's most likely that other devs intuitively use the delegate property of the base class, even if I comment the problem in the header file. If someone alters the delegate property the subclass won't do what it's supposed to do and I can't do anything to prevent that right now. And that's the point where i don't have a clue how to solve it.
What I tried is trying to override the delegate property to make it readonly like this:
@interface MySubclass : UIScrollView<UIScrollViewDelegate>
...
@property (nonatomic, assign, readonly) id<UIScrollViewDelegate>delegate;
@end
@implementation MySubclass
@property (nonatomic, assign, readwrite) id<UIScrollViewDelegate>delegate;
@end
That will result in a warning
"Attribute 'readonly' of property 'delegate' restricts attribute 'readwrite' of property inherited from 'UIScrollView'
Ok bad idea, as i'm obviously violating liskovs substitution principle here.
Next try --> Trying to override the delegate setter like this:
...
- (void) setDelegate(id<UIScrollViewDelegate>)newDelegate {
if (newDelegate != self) self.myOwnDelegate = newDelegate;
else _delegate = newDelegate; // <--- This does not work!
}
...
As commented, this example does not compile as it seems that the _delegate ivar wasn't found?! So i looked up the header file of UIScrollView and found this:
@package
...
id _delegate;
...
The @package directive restricts the access of the _delegate ivar to be accessible only by the framework itself. So when i want to set the _delegate ivar I HAVE TO use the synthesized setter. I can't see a way to override it in any way :( But i can't believe that there isn't a way around this, maybe i can't see the wood for the trees.
I appreciate for any hint on solving this problem.
Solution:
It works now with the solution of @rob mayoff . As i commented right below there was a problem with the scrollViewDidScroll: call. I finally did find out, what the problem is, even i don't understand why this is so :/
Right in the moment when we set the super delegate:
- (id) initWithFrame:(CGRect)frame {
...
_myDelegate = [[[MyPrivateDelegate alloc] init] autorelease];
[super setDelegate:_myDelegate]; <-- Callback is invoked here
}
there is a callback to _myDelegate. The debugger breaks at
- (BOOL) respondsToSelector:(SEL)aSelector {
return [self.userDelegate respondsToSelector:aSelector];
}
with the "scrollViewDidScroll:" selector as argument.
The funny thing at this time self.userDelegate isnt set yet and points to nil, so the return value is NO! That seems to cause that the the scrollViewDidScroll: methods won't get fired afterwards. It looks like a precheck if the method is implemented and if it fails this method won't get fired at all, even if we set our userDelegate property afterwards. I don't know why this is so, as the most other delegate methods don't have this precheck.
So my solution for this is, to invoke the [super setDelegate...] method in the PrivateDelegate setDelegate method, as this is the spot i'm pretty sure my userDelegate method is set.
So I'll end up with this implementation snippet:
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;
}
The rest of the code remains untouched. I'm still not really satisfied with this workaround, because it makes it necessary to call the setDelegate method at least once, but it works for my needs for the moment, although it feels very hacky :/
If someone has ideas how to improve that, I'd appreciate that.
Thanks @rob for your example!
For those of you looking to do this sort of thing with
UIControl
classes, see my STAControls project (specificallyBase
classes like this one). I have implemented delegate forwarding for the project using Rob Mayoff's answer.Another option is to subclass and use the power of abstract functions. For instance, you create
There you override some delegate method and define your abstract function
Now all you need to do is to subclass your EnhancedTableViewController and use your abstract functions instead of delegate ones. Like this:
Let me know if there is anything wrong here.
Thanks @robmayoff I encapsulated to be a more generic delegate interceptor: Having original MessageInterceptor class:
MessageInterceptor.h
MessageInterceptor.m
It is used in your generic delegate class:
GenericDelegate.h
GenericDelegate.m
So I can intercept any delegate I need to on any UITableView, UICollectionView, UIScrollView... :
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
Don't subclass it, encapsulate it instead ;)
Make a new UIView subclass - your header file would look like :
And just have a UIScrollView as a subview - your .m file would look like :
Inside your subclass, just always use
self.scrollView
and it will create the scroll view the first time you ask for it.This has the benefit of hiding the scroll view completely from anyone using your
MySubClass
- if you needed to change how it worked behind the scenes (i.e. change from a scroll view to a web view) it would be very easy to do :)It also means that no-one can alter how you want the scroll view to behave :)
PS I've assumed ARC - change
strong
toretain
and adddealloc
if necessary :)EDIT
If you want your class to behave exactly as a UIScrollView then you could try this adding this method (taken from these docs and untested!) :
There is a problem with making
MySubclass
its own delegate. Presumably you don't want to run custom code for all of theUIScrollViewDelegate
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: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
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 intoMyScrollView
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 ofUIScrollViewDelegate
. 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
Next we'll implement
MyScrollView
. It needs to create an instance ofMyScrollViewPrivateDelegate
, which it needs to own. Since aUIScrollView
doesn't own its delegate, we need an extra, strong reference to this object.We need to override
setDelegate:
anddelegate:
to store and return a reference to the user's delegate:We also need to define any extra methods that our private delegate might need to use:
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: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: