【问题标题】:How to subclass UIScrollView and make the delegate property private如何继承 UIScrollView 并使委托属性私有
【发布时间】:2012-04-16 05:20:31
【问题描述】:

这是我想要实现的目标:

我想子类化 UIScrollView 以获得额外的功能。这个子类应该能够对滚动做出反应,所以我必须将委托属性设置为 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

这样,当继承的滚动视图结束滚动时,我的子类可以做一些特殊的事情,但仍会通知事件的外部委托。到目前为止有效。但由于我想让这个子类对其他开发人员可用,我想限制对基类委托属性的访问,因为它应该只由子类使用。我认为其他开发人员很可能直观地使用了基类的委托属性,即使我在头文件中评论了问题。如果有人改变了委托属性,子类将不会做它应该做的事情,我现在不能做任何事情来阻止它。这就是我不知道如何解决它的地方。

我尝试覆盖委托属性以使其只读,如下所示:

@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 替换原则。

下一次尝试 --> 尝试像这样覆盖委托设置器:

...
- (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 时,我必须使用综合设置器。我看不到以任何方式覆盖它的方法 :( 但我不敢相信没有办法解决这个问题,也许我只见树木不见森林。

感谢您提供解决此问题的任何提示。


解决办法:

它现在可以与@rob mayoff 的解决方案一起使用。正如我在下面评论的那样,scrollViewDidScroll: 调用存在问题。我终于找到了,问题是什么,即使我不明白为什么会这样:/

就在我们设置超级委托的那一刻:

- (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 方法已设置的地方。

所以我最终会得到这个实现 sn-p:

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 你的例子!

【问题讨论】:

  • 如果代理响应scrollViewDidScroll:,似乎滚动视图缓存,可能是出于性能原因。要解决您必须设置用户委托的问题,请像以前一样在init 中设置它,在setDelegate 中您可以执行super.delegate = nil 然后super.delegate = self.internalDelegate 强制它重新检查。

标签: objective-c ios delegates uiscrollview subclass


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • 真是太棒了!!感谢这段代码,它似乎工作得很好。但是我对这种方法有一个新问题,我不太明白,这很奇怪。
  • 当我像您在示例中所做的那样将委托设置为 _myDelegate 时,即使正确实施,我的私有委托类也没有获得 viewDidScroll 委托方法。奇怪的是,我收到了其他调用,例如 scrollViewWillBeginDragging: 和 scrollViewWillBeginDecelerating:。出于测试目的,我再次将委托设置为 self(我的子类),并在那里实现了 viewdidscroll 方法。并在那里调用该方法。当我滚动滚动视图时,我根本不明白为什么 viewDidScroll 事件无法识别甚至被触发......:(
  • UIScrollViewDelegate 没有声明名为 viewDidScroll 的方法。也许您正在覆盖私有 UIScrollView 方法。委托方法名为scrollViewDidScroll:
  • 请注意,如果您只需要回复scrollViewDidScroll:,您也可以覆盖UIScrollView 并使用layoutSubviews:。每当边界发生变化时都会调用它,这就是滚动视图的滚动方式。刚刚在 WWDC 2011 会议 104 中遇到了这个问题。
  • @Awesome-o 当你调用setDelegate:(或者当XIB加载器调用它时),滚动视图立即将respondsToSelector:发送给@987654350定义的几个选择器的新委托@ 协议。滚动视图保存返回的布尔值,因此以后不必再次询问。您可以通过在滚动视图委托中覆盖 respondsToSelector: 来轻松地自行检查。
【解决方案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 之前的回答

【讨论】:

    【解决方案3】:

    另一种选择是子类化并使用抽象函数的强大功能。 例如,您创建

    @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
    };
    

    如果这里有什么问题,请告诉我。

    【讨论】:

      【解决方案4】:

      不要子类化它,而是封装它;)

      创建一个新的 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];
      }
      

      【讨论】:

      • 好的,我也想过这个,但是这种方法的缺点是你必须重新实现整个 uiscrollview api,以防你想让子类像它的基类一样工作。这仍然可以通过使用 forwardInvocation:(NSInvocation*) 调用将所有未知方法调用转发到另一个目标来解决。这里的问题是,根据调用的消息,我有两个目标要转发到。如果它是一个委托方法(例如scrollViewDid ...),我想转发到“外部”委托,如果它是滚动视图的实例方法,我想转发到滚动视图。
      • 啊,我没有意识到您希望它的行为与滚动视图完全一样。我的编辑应该让你的行为大部分像我认为的 UIScrollView。
      【解决方案5】:

      对于那些希望通过 UIControl 类做此类事情的人,请参阅我的 STAControls project(特别是 Baselike this one)。我已经使用Rob Mayoff's answer 为项目实现了委托转发。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-09-04
        • 2011-07-12
        • 2023-03-09
        • 2017-08-13
        • 2015-06-20
        • 2014-09-17
        相关资源
        最近更新 更多