协议与代理商的异形变换

  • 时间:2022-03-15 14:33 作者:01_Jack 来源: 阅读:345
  • 扫一扫,手机访问
摘要:前言一个类/对象只需遵守某个协议即可以调用协议方法,从而在某些方面达成共识。假如单纯遵守协议并实现协议方法,在某些场景从外部调用,这篇文章已经没有存在的必要了。当协议与代理商配合使用时,可以组成代理商模式。在iOS中有这么一句话,“代理商是一对一的”,本文围绕这句话开展,并给出不同场景下的多种处理方案及终

前言

一个类/对象只需遵守某个协议即可以调用协议方法,从而在某些方面达成共识。假如单纯遵守协议并实现协议方法,在某些场景从外部调用,这篇文章已经没有存在的必要了。当协议与代理商配合使用时,可以组成代理商模式。在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结构体的成员变量很简单,只有方法名与参数

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
JKProtocolHelper.gif

Demo已放到github,自取

Have fun!

  • 全部评论(0)
最新发布的资讯信息
【系统环境|】2FA验证器 验证码如何登录(2024-04-01 20:18)
【系统环境|】怎么做才能建设好外贸网站?(2023-12-20 10:05)
【系统环境|数据库】 潮玩宇宙游戏道具收集方法(2023-12-12 16:13)
【系统环境|】遥遥领先!青否数字人直播系统5.0发布,支持真人接管实时驱动!(2023-10-12 17:31)
【系统环境|服务器应用】克隆自己的数字人形象需要几步?(2023-09-20 17:13)
【系统环境|】Tiktok登录教程(2023-02-13 14:17)
【系统环境|】ZORRO佐罗软件安装教程及一键新机使用方法详细简介(2023-02-10 21:56)
【系统环境|】阿里云 centos 云盘扩容命令(2023-01-10 16:35)
【系统环境|】补单系统搭建补单源码搭建(2022-05-18 11:35)
【系统环境|服务器应用】高端显卡再度登上热搜,竟然是因为“断崖式”的降价(2022-04-12 19:47)
手机二维码手机访问领取大礼包
返回顶部