objective-c - 如何继承 UIScrollView 并将委托(delegate)属性设为私有(private)

标签 objective-c ios delegates uiscrollview subclass

这是我想要实现的:

我想子类化 UIScrollView 以获得额外的功能。这个子类应该能够对滚动使用react,所以我必须将委托(delegate)属性设置为 self 以接收如下事件:

- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { ... }

另一方面,其他类也应该仍然能够接收这些事件,就像它们正在使用基本的 UIScrollView 类一样。

所以我对如何解决这个问题有不同的想法,但所有这些都不完全让我满意:(

我的主要方法是..像这样使用自己的委托(delegate)属性:

@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 结束滚动时,我的子类可以做一些特殊的事情,但仍然通知事件的外部委托(delegate)。到目前为止有效。但是因为我想让其他开发人员可以使用这个子类,所以我想限制对基类委托(delegate)属性的访问,因为它只能由子类使用。我认为很可能是其他开发者凭直觉使用了基类的委托(delegate)属性,即使我在头文件中注释了这个问题。如果有人改变委托(delegate)属性,子类将不会做它应该做的事情,我现在无法做任何事情来阻止它。这就是我不知道如何解决它的地方。

我尝试的是尝试覆盖委托(delegate)属性,使其像这样只读:

@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'

好吧,坏主意,因为我在这里显然违反了里氏替换原则。

下一步尝试 --> 尝试像这样覆盖委托(delegate) setter :

...
- (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: 调用存在问题。我终于找到了,问题是什么,即使我不明白为什么会这样:/

就在我们设置 super 委托(delegate)的那一刻:

- (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 属性。我不知道为什么会这样,因为大多数其他委托(delegate)方法没有这种预检查。

所以我的解决方案是,在 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 方法,但它目前可以满足我的需要,虽然感觉很老套:/

如果有人有改进的想法,我将不胜感激。

感谢@rob 的示例!

最佳答案

使 MySubclass 成为自己的委托(delegate)存在问题。假设您不想为 所有 UIScrollViewDelegate 方法运行自定义代码,但是无论您是否有自己的实现,都必须将消息转发给用户提供的委托(delegate)或不。所以你可以尝试实现所有的委托(delegate)方法,其中大部分只是像这样转发:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    [self.myOwnDelegate scrollViewDidZoom:scrollView];
}

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

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

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

MyScrollView.h

@interface MyScrollView : UIScrollView
@end

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

MyScrollView.m

@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
    id<UIScrollViewDelegate> _userDelegate;
}
@end

接下来我们将实现MyScrollView。它需要创建它需要拥有的 MyScrollViewPrivateDelegate 实例。由于 UIScrollView 不拥有它的委托(delegate),我们需要对此对象的额外强引用。

@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: 来存储和返回对用户委托(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;
}

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

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

@end

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

@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

关于objective-c - 如何继承 UIScrollView 并将委托(delegate)属性设为私有(private),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9978279/

相关文章:

ios - iOS SwiftUI中ForEach内部的if语句

c# - 在不指定参数的情况下使用强类型方法作为参数

iphone - 委托(delegate)被派往错误的类(class)

ios - 解释 iPhone 崩溃日志/堆栈跟踪

iphone编程: collision detection of a UIImageView and an animation

ios - 如何在 Swift 自动化测试中生成 TOTP 代码?

IOS dispatch_get_main_queue()被多次调用

ios - 各种UIgesture是否优先于touchbagan?

c# - 委托(delegate){}是什么意思

c# - 什么构成 'redundant delegate creation' ?