如何子类化UIScrollView并将委托属性设为私有

时间:2012-04-02 14:30:03

标签: objective-c ios delegates uiscrollview subclass

这是我想要实现的目标:

我想将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为你的榜样!

5 个答案:

答案 0 :(得分:43)

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文件中完成。首先,我们定义私有委托类的接口。它的工作是回调MyScrollView某些委托方法,并将所有消息转发给用户的委托。所以我们只想给它作为UIScrollViewDelegate一部分的方法。我们不希望它有额外的方法来管理对用户委托的引用,所以我们只将该引用保留为实例变量:

MyScrollView.m

@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为项目实现了委托转发。