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

Mex*_*xyn 26 delegates subclass objective-c uiscrollview ios

这是我想要实现的目标:

我想子类化UIScrollView以获得其他功能.这个子类应该能够对滚动作出反应,所以我必须将delegate属性设置为self来接收如下事件:

- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { ... }
Run Code Online (Sandbox Code Playgroud)

另一方面,其他类仍然应该能够接收这些事件,就像它们使用基础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
Run Code Online (Sandbox Code Playgroud)

这样,当继承的scrollview结束滚动时,我的子类可以执行一些特殊操作,但仍然会通知事件的外部委托.这到目前为止工作.但是因为我想让这个子类可供其他开发人员使用,我想限制对基类委托属性的访问,因为它只应由子类使用.我认为其他开发人员最有可能直观地使用基类的委托属性,即使我在头文件中注释问题.如果有人改变委托属性,子类将不会做它应该做的事情,我现在无法阻止它.这就是我不知道如何解决它的问题.

我尝试尝试覆盖委托属性,使其只读取:

@interface MySubclass : UIScrollView<UIScrollViewDelegate>
...
@property (nonatomic, assign, readonly) id<UIScrollViewDelegate>delegate;
@end

@implementation MySubclass
@property (nonatomic, assign, readwrite) id<UIScrollViewDelegate>delegate;
@end
Run Code Online (Sandbox Code Playgroud)

这将导致警告

"Attribute 'readonly' of property 'delegate' restricts attribute 'readwrite' of property inherited from 'UIScrollView'
Run Code Online (Sandbox Code Playgroud)

好主意,因为我明显违反了liskovs替换原则.

接下来尝试 - >尝试覆盖委托设置器,如下所示:

...
- (void) setDelegate(id<UIScrollViewDelegate>)newDelegate {
    if (newDelegate != self) self.myOwnDelegate = newDelegate;
    else _delegate = newDelegate; // <--- This does not work!
}
...
Run Code Online (Sandbox Code Playgroud)

如评论所示,此示例无法编译,因为似乎找不到_delegate ivar?!所以我查找了UIScrollView的头文件,发现了这个:

@package
    ...
    id           _delegate;
...
Run Code Online (Sandbox Code Playgroud)

@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
}
Run Code Online (Sandbox Code Playgroud)

有一个回调_myDelegate.调试器中断了

- (BOOL) respondsToSelector:(SEL)aSelector {
    return [self.userDelegate respondsToSelector:aSelector];
}
Run Code Online (Sandbox Code Playgroud)

使用"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;
}
Run Code Online (Sandbox Code Playgroud)

其余代码保持不变.我仍然不满意这种解决方法,因为它使得必须至少调用一次setDelegate方法,但它现在适用于我的需求,虽然感觉非常hacky:/

如果有人有想法如何改进,我会很感激.

谢谢你@rob为你的榜样!

rob*_*off 43

制作MySubclass自己的委托时出现问题.想必你不想为运行自定义代码的所有UIScrollViewDelegate方法,但你有你是否有自己的实现或不将消息转发给用户提供的委托.所以你可以尝试实现所有的委托方法,其中大多数只是像这样转发:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    [self.myOwnDelegate scrollViewDidZoom:scrollView];
}
Run Code Online (Sandbox Code Playgroud)

这里的问题是有时新版本的iOS会添加新的委托方法.例如,iOS 5.0添加了scrollViewWillEndDragging:withVelocity:targetContentOffset:.所以你的scrollview子类将不会是面向未来的.

处理此问题的最佳方法是创建一个单独的私有对象,该对象仅充当您的scrollview的委托,并处理转发.此专用委托对象可以将收到的每条消息转发给用户提供的委托,因为它只接收委托消息.

这是你做的.在头文件中,您只需要为scrollview子类声明接口.您不需要公开任何新方法或属性,因此它看起来像这样:

MyScrollView.h

@interface MyScrollView : UIScrollView
@end
Run Code Online (Sandbox Code Playgroud)

所有实际工作都在.m文件中完成.首先,我们定义私有委托类的接口.它的工作是回调MyScrollView一些委托方法,并将所有消息转发给用户的委托.所以我们只想给它一些方法UIScrollViewDelegate.我们不希望它有额外的方法来管理对用户委托的引用,所以我们只将该引用保留为实例变量:

MyScrollView.m

@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
    id<UIScrollViewDelegate> _userDelegate;
}
@end
Run Code Online (Sandbox Code Playgroud)

接下来我们将实施MyScrollView.它需要创建一个MyScrollViewPrivateDelegate它需要拥有的实例.由于a 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];
}
Run Code Online (Sandbox Code Playgroud)

我们需要覆盖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;
}
Run Code Online (Sandbox Code Playgroud)

我们还需要定义我们的私有委托可能需要使用的任何额外方法:

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

@end
Run Code Online (Sandbox Code Playgroud)

现在我们可以最终定义实现了MyScrollViewPrivateDelegate.我们需要明确定义应包含私有自定义代码的每个方法.该方法需要执行我们的自定义代码,并将消息转发给用户的委托,如果用户的委托响应消息:

@implementation MyScrollViewPrivateDelegate

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [(MyScrollView *)scrollView myScrollViewDidEndDecelerating];
    if ([_userDelegate respondsToSelector:_cmd]) {
        [_userDelegate scrollViewDidEndDecelerating:scrollView];
    }
}
Run Code Online (Sandbox Code Playgroud)

我们需要处理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
Run Code Online (Sandbox Code Playgroud)

  • 请注意,在您只需要响应`scrollViewDidScroll:`的情况下,您也可以覆盖UIScrollView并使用`layoutSubviews:`.每当边界改变时调用它,这是滚动视图滚动的方式.刚刚在WWDC 2011会议104中提到了这一点. (2认同)