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!
See Question&Answers more detail:
os