这是我想要实现的目标:
我想将UIScrollView子类化以获得其他功能。这个子类应该能够对滚动作出反应,所以我必须将delegate属性设置为self来接收以下事件:
- (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
这样,当继承的scrollview结束滚动时,我的子类可以执行一些特殊操作,但仍然会通知事件的外部委托。这到目前为止工作。但是因为我想让这个子类可供其他开发人员使用,我想限制对基类委托属性的访问,因为它只应由子类使用。我认为其他开发人员最有可能直观地使用基类的委托属性,即使我在头文件中注释问题。如果有人改变委托属性,子类将不会做它应该做的事情,我现在无法阻止它。这就是我不知道如何解决它的问题。
我尝试的是尝试覆盖委托属性,使其只读:
@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'
好的主意,因为我明显违反了liskovs替换原则。
接下来尝试 - &gt;试图像这样覆盖委托制定者:
...
- (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指令限制_delegate ivar的访问权限只能由框架本身访问。因此,当我想设置_delegate ivar时,我必须使用合成的setter。我无法看到以任何方式覆盖它的方法:(但我无法相信没有办法解决这个问题,也许我看不到树木的木材。
我很感激任何解决这个问题的暗示。
现在可以使用@rob mayoff的解决方案。正如我在下面评论的那样,scrollViewDidScroll:call出现了问题。我终于找到了,问题是什么,即使我不明白为什么会这样:/
在我们设置超级代表的那一刻:
- (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尚未设置并指向nil,因此返回值为NO!这似乎导致之后不会触发scrollViewDidScroll:方法。如果实现该方法,它看起来像预先检查,如果失败,这个方法根本不会被触发,即使我们之后设置了userDelegate属性。我不知道为什么会这样,因为大多数其他委托方法没有预先检查。
所以我的解决办法就是调用PrivateDelegate setDelegate方法中的[super setDelegate ...]方法,因为这是我非常确定我的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方法,但它现在可以满足我的需求,虽然它感觉非常hacky:/
如果有人有想法如何改进,我会很感激。
感谢@rob为你的榜样!
答案 0 :(得分:43)
将MySubclass
作为自己的委托是有问题的。据推测,您不希望为UIScrollViewDelegate
方法的所有运行自定义代码,但是您必须将消息转发给用户提供的委托,无论您是否拥有自己的实现。因此,您可以尝试实现所有委托方法,其中大多数只是像这样转发:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self.myOwnDelegate scrollViewDidZoom:scrollView];
}
这里的问题是有时新版本的iOS会添加新的委托方法。例如,iOS 5.0添加了scrollViewWillEndDragging:withVelocity:targetContentOffset:
。所以你的scrollview子类将不会是面向未来的。
处理此问题的最佳方法是创建一个单独的私有对象,该对象仅充当您的scrollview的委托,并处理转发。此专用委托对象可以将收到的每条消息转发给用户提供的委托,因为仅接收委托消息。
这是你做的。在头文件中,您只需要为scrollview子类声明接口。您不需要公开任何新方法或属性,因此它看起来像这样:
@interface MyScrollView : UIScrollView
@end
所有实际工作都在.m
文件中完成。首先,我们定义私有委托类的接口。它的工作是回调MyScrollView
某些委托方法,并将所有消息转发给用户的委托。所以我们只想给它作为UIScrollViewDelegate
一部分的方法。我们不希望它有额外的方法来管理对用户委托的引用,所以我们只将该引用保留为实例变量:
@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
id<UIScrollViewDelegate> _userDelegate;
}
@end
接下来我们将实施MyScrollView
。它需要创建一个需要拥有的MyScrollViewPrivateDelegate
实例。由于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
答案 1 :(得分: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之前的回答
答案 2 :(得分:1)
另一个选择是子类化并使用抽象函数的强大功能。 例如,您创建
@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
};
请告诉我这里是否有任何问题。
答案 3 :(得分:0)
不要对它进行子类化,而是将其封装起来;)
创建一个新的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
的任何人身上 - 如果您需要更改其在幕后的工作方式(即从滚动视图更改为Web视图),它将非常容易做到:)
这也意味着没有人可以改变滚动视图的行为方式:)
PS我假设ARC - 将strong
更改为retain
并在必要时添加dealloc
:)
修改
如果您希望您的班级完全作为UIScrollView,那么您可以尝试添加此方法(取自these docs并且未经测试!):
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([self.scrollView respondsToSelector:anInvocation.selector])
[anInvocation invokeWithTarget:self.scrollView];
else
[super forwardInvocation:anInvocation];
}
答案 4 :(得分:0)
对于那些希望使用UIControl
课程执行此类操作的人,请参阅我的STAControls project(特别是Base
班级like this one)。我使用Rob Mayoff's answer为项目实现了委托转发。