一个类/对象只需遵守某个协议即可以调用协议方法,从而在某些方面达成共识。假如单纯遵守协议并实现协议方法,在某些场景从外部调用,这篇文章已经没有存在的必要了。当协议与代理商配合使用时,可以组成代理商模式
。在iOS中有这么一句话,“代理商是一对一的”,本文围绕这句话开展,并给出不同场景下的多种处理方案及终极处理方案。
场景:假设JKScrollView继承自UIScrollView,JKScrollView对外提供的接口需要用到UIScrollViewDelegate,此时需要将JKScrollView对应实例对象的代理商设置为自身。这样一来,当外部重设代理商时,会导致内部代理商失效。怎么在确保内部代理商正常的前提下,外部依然可以获取代理商相应的功能?
最简单的写法大概长这样:
NS_ASSUME_NONNULL_BEGIN@class JKScrollView;typedef void(^JKScrollViewDidScrollBlock)(JKScrollView *scrollView);@interface JKScrollView : UIScrollView<UIScrollViewDelegate>@property (nullable, nonatomic, copy) JKScrollViewDidScrollBlock didScrollBlock;@endNS_ASSUME_NONNULL_END
- (instancetype)init { if (self = [super init]) { [super setDelegate:self]; } return self;}- (void)setDelegate:(id<UIScrollViewDelegate>)delegate { if (!delegate || self.delegate == delegate) return; [super setDelegate:self];}- (void)setDidScrollBlock:(JKScrollViewDidScrollBlock)didScrollBlock { if (!didScrollBlock) return; _didScrollBlock = [didScrollBlock copy];}- (void)scrollViewDidScroll:(UIScrollView *)scrollView { !_didScrollBlock ? : _didScrollBlock(self); // do something}
1.在初始化时,将代理商设为self
2.重写代理商的setter,强制将代理商设为self(以防外部改变delegate)
3.在代理商方法中执行外部传入的block
这样写的确可以完成相应需求,但是需要手动增加block,假如不想增加block也可以这么写
NS_ASSUME_NONNULL_BEGIN@interface JKTableScrollView : UIScrollView@property (nullable, nonatomic, weak, readonly) id<UIScrollViewDelegate> fakeDelegate;@endNS_ASSUME_NONNULL_END
- (instancetype)init { if (self = [super init]) { [super setDelegate:self]; } return self;}- (void)setDelegate:(id<UIScrollViewDelegate>)delegate { if (!delegate || self.delegate == delegate) return; _fakeDelegate = delegate; [super setDelegate:self];}- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (_fakeDelegate && [_fakeDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { [_fakeDelegate scrollViewDidScroll:scrollView]; } // do something}
与第一种方法略有不同,在delegate的setter中,将外部设置的delegate保存,并在内部代理商方法调用时判断外部代理商能否实现相同方法,假如实现,则手动调用。
这两种写法思想相似,并且写起来都很麻烦,特别当有多个代理商方法要实现,会做很多无用功。
而且会有这么一种情况:
内部并不需要用到代理商的某个方法,但是外部需要用到,此时在内部还得写相应的代理商方法用来适配外部代理商调用。
为了减少这种适配,现在引入第三种写法。
在详情第三种写法前,请先确保理解消息转发流程,假如不理解,可以看这篇文章传送门
除此之外,还会用到runtime中的这个函数
struct objc_method_description protocol_getMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull aSel, BOOL isRequiredMethod, BOOL isInstanceMethod)
可以看到,objc_method_description
结构体的成员变量很简单,只有方法名与参数
说明:这种方法在第二种方法的基础上增加如下代码
- (BOOL)respondsToSelector:(SEL)aSelector { BOOL result = [super respondsToSelector:aSelector]; if (!result && _fakeDelegate) { struct objc_method_description omd = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), aSelector, NO, YES); if (omd.name) { result = [_fakeDelegate respondsToSelector:aSelector]; } } return result;}- (id)forwardingTargetForSelector:(SEL)aSelector { if (_fakeDelegate) { struct objc_method_description omd = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), aSelector, NO, YES); if (omd.name) { return _fakeDelegate; } } return [super forwardingTargetForSelector:aSelector];}
重写respondsToSelector :
方法,满足代码中条件则认为可响应。若某代理商方法内部未实现,而外部代理商实现(fakeDelegate),因为真实代理商为内部代理商(self),正常流程fakeDelegate无法响应。重写respondsToSelector :
后会进入消息转发流程,在forwardingTargetForSelector:
判断能否满足条件,假如满足则转发给_fakeDelegate,从而使_fakeDelegate可以响应内部未实现的代理商方法。
这种写法相对前两种方法大大简化了实现外部代理商的书写流程,但还是很烦,由于内部代理商已实现的方法没法进入消息转发流程,所以只需内部代理商实现的方法,外部代理商想实现就必需在内部做一次判断。
同时还有这么两种情况:
假如外部要实现的代理商有多个怎样办?
假如内部并不需要实现代理商,外部需要实现多个代理商又怎样办?
接着方法三的思路来,能否能在消息转发流程将要实现的代理商方法转发给多个外部代理商对象?显然是可以的,由于消息转发的最后一步forwardInvocation:
参数为NSInvocation
,而NSInvocation可以指定target。假如不熟习NSInvocation,可以看这篇文章传送门
在开始终极写法前,还需要处理一个问题:targets由外部传入,假如直接用NSArray保存,而保存的对象又被其余对象持有,此时极易导致循环引用。因而,这里用NSPointerArray
代替NSArray
以防循环引用
直接上代码:
#import "ViewController.h"#import "JKProtocolHelper.h"@interface Test: NSObject<UIScrollViewDelegate>@end@implementation Test- (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSLog(@"Test");}@end@interface ViewController ()<UITableViewDelegate>@property (nonatomic, strong) Test *test;@property (nonatomic, strong) id helper;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; [self.view addSubview:tableView]; _test = [Test new]; _helper = [JKProtocolHelper helperWithProtocol:@protocol(UIScrollViewDelegate) executors:@[self, _test]]; tableView.delegate = _helper;}- (void)dealloc { NSLog(@"%s", __func__);}- (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSLog(@"view controller");}@end
Demo已放到github,自取
Have fun!