美文网首页
IOS基础使用:Delegate、Protocol

IOS基础使用:Delegate、Protocol

作者: 时光啊混蛋_97boy | 来源:发表于2021-04-03 13:40 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、比较
      • 1、代理和block的选择
      • 2、Delegate、Notification、KVO的优缺点比较
    • 二、跨层传值
      • 1、属性正向传值
      • 2、KVC正向传值
      • 3、Delegate逆向传值
      • 4、Block逆向传值
      • 5、KVO逆向传值
      • 6、Notification反向传值
      • 7、NSUserDefaults传值
    • 三、Delegate与Protocol的用法
      • 1、简介
      • 2、计算人数工具类
      • 3、员工
      • 4、技术总监
      • 5、调用方式
    • Demo
    • 参考文献

    一、比较

    1、代理和block的选择

    多个消息传递,应该使用delegate,这个时候block反而不便于维护,而且看起来非常臃肿,很别扭。如果UITableView中很多代理都换成block实现,我们脑海里想一下这个场景是很可怕的。

    一个委托对象的代理属性只能有一个代理对象,如果想要委托对象调用多个代理对象的回调应该用block,因为delegate只是一个保存某个代理对象的地址,如果设置多个代理相当于重新赋值,只有最后一个设置的代理才会被真正赋值。

    单例对象最好不要用delegate。单例对象由于始终都只是同一个对象,如果使用delegate,就会造成我们上面说的delegate属性被重新赋值的问题,最终只能有一个对象可以正常响应代理方法。

    代理是可选的,而block在方法调用的时候只能通过将某个参数传递一个nil进去实现同样的效果,只不过这并不是什么大问题,没有代码洁癖的可以忽略。

    从设计模式的角度来说,代理更佳面向过程,而block更佳面向结果。例如我们使用NSXMLParserDelegate代理进行XML解析,NSXMLParserDelegate中有很多代理方法,NSXMLParser会不间断调用这些方法将一些转换的参数传递出来,这就是NSXMLParser解析流程,这些通过代理来展现比较合适。而例如一个网络请求回来,就通过successfailure代码块来展示就比较好。

    从性能上来说,block的性能消耗要略大于delegate,因为block会涉及到栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理。而代理只是定义了一个方法列表,在运行时向遵守协议的对象发送消息即可。


    2、Delegate、Notification、KVO的优缺点比较

    Delegate
    优势
    • 如果delegate中的一个方法没有实现那么就会出现编译警告/错误
    • 一个控制器中可以实现多个不同的协议
    • 能够接收返回值
    • 一对一的通信
    缺点: 需要定义很多代码
    • 协议定义
    • controllerdelegate属性
    • 实现delegate方法
    Notification
    优势
    • 代码量少,实现简单
    • 1对多的通信
    • 可以携带自定义消息
    缺点
    • 需要在不用的时候注销通知
    • 调试难以追踪
    • 通知发送后,不能从观察者得到任何反馈信息
    • 代码可读性不强
    • notifacationName必须相同,否则无法接受消息
    KVO
    优势
    • key paths来观察属性,因此可以观察嵌套对象
    • 能够提供一种简单的方法实现两个对象间的同步
    • 能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
    缺点
    • 观察的属性必须使用 string 来定义,因此编译器不会出现警告
    • 对属性重构将导致我们的观察代码不再可用

    二、跨层传值

    1、属性正向传值

    当从第一个页面push到第二个页面时,第二个页面需要使用到第一个页面的数据,这时就可以使用正向传值。

    界面一

    这样传递是有问题的,因为子页面中的textfield是在viewDidLoad中进行初始化和布局的,但在这时候textfield还没有初始化,为nil,所以赋值是失效的。

    - (void)proprety
    {
        SecondViewController *postVC = [[SecondViewController alloc] init];
        postVC.content = @"刘盈池";
        postVC.contentTextField.text = @"谢佳培";
        
        [self.navigationController pushViewController:postVC animated:YES];
    }
    
    界面二
    - (void)proprety
    {
        NSLog(@"属性正向传值,content内容为:%@",self.content);
        NSLog(@"属性正向传值,contentTextField内容为:%@",self.contentTextField.text);
    }
    

    输出结果为:

    2020-09-23 17:03:02.553646+0800 Demo[92767:17573694] 属性正向传值,content内容为:刘盈池
    2020-09-23 17:03:02.553825+0800 Demo[92767:17573694] 属性正向传值,contentTextField内容为:
    

    2、KVC正向传值

    通过Key名给对象的属性赋值,而不需要调用明确的存取方法,这样就可以在运行时动态地访问和修改对象的属性。

    界面一
    - (void)useKVC
    {
        SecondViewController *postVC = [[SecondViewController alloc] init];
        [postVC setValue:@"刘盈池" forKey:@"content"];
        
        [self.navigationController pushViewController:postVC animated:YES];
    }
    
    界面二
    - (void)useKVC
    {
        NSLog(@"KVC正向传值,content内容为:%@",self.content);
    }
    

    输出结果为:

    2020-09-23 17:57:13.984068+0800 Demo[93502:17615940] KVC正向传值,content内容为:刘盈池
    

    3、Delegate逆向传值

    在从第二个页面返回第一个页面的时候,第二个页面会释放掉内存,如果需要使用子页面中的数据就用到了逆向传值。

    界面一

    在第一个页面中遵从该代理。

    @interface FirstViewController ()<contentDelegate>
    

    第二个页面的代理是第一个页面自身self

    - (void)useDelegate
    {
        SecondViewController *postVC = [[SecondViewController alloc] init];
        postVC.delegate = self;
        [self.navigationController pushViewController:postVC animated:YES];
    }
    

    实现代理中定义的方法,第二个页面调用的时候会回调该方法。在方法的实现代码中将参数传递给第一个页面的属性

    - (void)transferString:(NSString *)content
    {
        self.title = content;
        NSLog(@"Delegate反向传值,第一个页面接收到的content为:%@",content);
    }
    
    界面二

    声明代理

    @protocol contentDelegate <NSObject>
    
    /** 代理方法 */
    - (void)transferString:(NSString *)content;
    
    @end
    

    声明代理属性

    @interface SecondViewController : UIViewController
    
    @property(nonatomic, weak) id<contentDelegate> delegate;
    
    @end
    

    返回第一个页面之前调用代理中定义的数据传递方法,方法参数就是要传递的数据。如果当前的代理存在,并且实现了代理方法,则调用代理方法进行传递数据。

    - (void)backDelegate
    {
        if (self.delegate && [self.delegate respondsToSelector:@selector(transferString:)])
        {
            [self.delegate transferString:@"刘盈池"];
            [self.navigationController popViewControllerAnimated:YES];
        }
    }
    

    输出结果为:

    2020-09-23 17:21:45.923769+0800 Demo[92974:17584680] Delegate反向传值,第一个页面接收到的content为:刘盈池
    

    4、Block逆向传值

    界面一

    通过子页面的block回传拿到数据后进行处理,赋值给当前页面的textfield

    - (void)useBlock
    {
        SecondViewController *postVC = [[SecondViewController alloc] init];
        postVC.transDataBlock = ^(NSString * _Nonnull content) {
            self.title = content;
            NSLog(@"Block逆向传值,第一个页面接收到的content为:%@",content);
        };
        
        [self.navigationController pushViewController:postVC animated:YES];
    }
    
    界面二

    声明block,用于回传数据

    typedef void(^TransDataBlock)(NSString *content);
    

    定义一个block属性,用于回传数据

    @property(nonatomic, copy) TransDataBlock transDataBlock;
    
    @property (nonatomic, copy) void(^ TransDataBlock)(NSString * content);
    

    调用block并将数据作为参数回传。

    - (void)backBlock
    {
        if (self.transDataBlock)
        {
            self.transDataBlock(@"刘盈池");
        }
        [self.navigationController popViewControllerAnimated:YES];
    }
    

    输出结果为:

    2020-09-23 17:29:40.121002+0800 Demo[93133:17594231] Block逆向传值,第一个页面接收到的content为:刘盈池
    

    5、KVO逆向传值

    界面一

    在第一个页面注册观察者。

    - (void)useKVO
    {
        self.secondVC = [[SecondViewController alloc] init];
    
        [self.secondVC addObserver:self forKeyPath:@"content" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
        
        [self.navigationController pushViewController:self.secondVC animated:YES];
    }
    

    实现KVO的回调方法,当观察者中的数据有变化时会回调该方法。

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if ([keyPath isEqualToString:@"content"])
        {
            self.title = self.secondVC.content;
            NSLog(@"KVO反向传值,第一个页面接收到的content为:%@",self.secondVC.content);
        }
    }
    

    在第一个页面销毁时移除KVO观察者。

    - (void)dealloc
    {
        [self.secondVC removeObserver:self forKeyPath:@"content"];
    }
    
    界面二
    - (void)backKVO
    {
        // 修改属性的内容
        self.content = @"刘盈池";
    
        // 返回第一个界面回传数据
        [self.navigationController popViewControllerAnimated:YES];
    }
    

    输出结果为:

    2020-09-23 17:46:10.744468+0800 Demo[93363:17607725] KVO反向传值,第一个页面接收到的content为:刘盈池
    

    6、Notification反向传值

    界面一

    点击跳转到第二个页面

    - (void)notificationClick
    {
        SecondViewController *postVC = [[SecondViewController alloc] init];
        [self.navigationController pushViewController:postVC animated:YES];
    }
    

    注册通知

    - (void)registerNotification
    {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(infoAction:) name:@"info" object:nil];
    }
    

    实现收到通知时触发的方法

    - (void)infoAction:(NSNotification *)notification
    {
        NSLog(@"接收到通知,内容为:%@",notification.userInfo);
        self.title = notification.userInfo[@"name"];
    }
    

    在注册通知的页面消毁时一定要移除已经注册的通知,否则会造成内存泄漏。

    - (void)dealloc
    {
        // 移除所有通知
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        
        // 移除某个通知
        [[NSNotificationCenter defaultCenter] removeObserver:self name:@"info" object:nil];
        
        // 在第一个页面销毁时移除KVO观察者
        [self.secondVC removeObserver:self forKeyPath:@"content"];
    }
    
    界面二

    如果发送的通知指定了object对象,那么观察者接收的通知设置的object对象与其一样,才会接收到通知,但是接收通知如果将这个参数设置为了nil,则会接收一切通知。

    // 返回到上个界面
    - (void)backNotification
    {
        // 1.创建字典,将数据包装到字典中
        NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"刘盈池",@"name",@"19",@"age", nil];
        
        // 2.创建通知
        NSNotification *notification = [NSNotification notificationWithName:@"info" object:nil userInfo:dict];
        
        // 3.通过通知中心发送通知
        [[NSNotificationCenter defaultCenter] postNotification:notification];
        
        // 4.回传数据
        [self.navigationController popViewControllerAnimated:YES];
    }
    

    输出结果为:

    2020-09-23 18:08:28.526933+0800 Demo[93661:17626006] 接收到通知,内容为:{
        age = 19;
        name = "\U5218\U76c8\U6c60";
    }
    

    7、NSUserDefaults传值

    页面一

    需要使用值时通过NSUserDefaults从沙盒目录里面取值进行处理。

    - (void)useDefaults
    {
        NSString *content = [[NSUserDefaults standardUserDefaults] valueForKey:@"Girlfriend"];
        NSLog(@"NSUserDefaults传值,内容为:%@",content);
    }
    
    页面二

    需要传值时将数据通过NSUserDefaults保存到沙盒目录里面,比如用户名之类,当用户下次登录或者使用app的时候,可以直接从本地读取此值。

    - (void)userDefaultsClick
    {
        [[NSUserDefaults standardUserDefaults] setObject:@"刘盈池" forKey:@"Girlfriend"];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 跳转到第一个界面
        [self.navigationController popViewControllerAnimated:YES];
    }
    

    输出结果为:

    2020-09-23 18:19:10.065815+0800 Demo[93846:17636600] NSUserDefaults传值,内容为:刘盈池
    

    三、Delegate与Protocol的用法

    1、简介

    Delegate
    什么是Delegate与Protocol呢?

    举个简单的例子,外卖app就是我的代理,我就是委托方,我买了一瓶红茶并付给外卖app钱,这就是购买协议。我只需要从外卖app上购买就可以,具体的操作都由外卖app去处理,我只需要最后接收这瓶红茶就可以。我付的钱就是参数,最后送过来的红茶就是处理结果。

    有哪些特性呢?

    如果只是某个类使用,我们常做的就是写在某个类中。如果多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。

    协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。

    默认是@required状态的,无论是@optional还是@required,在委托方调用代理方法时都需要做一个判断,判断代理是否实现当前方法,否则会导致崩溃。

    原理是什么呢?

    其实委托方的代理属性本质上就是代理对象自身,设置委托代理就是代理属性指针指向代理对象,相当于代理对象只是在委托方中调用自己的方法,如果方法没有实现就会导致崩溃。

    为什么我们设置代理属性都使用weak呢?

    由于代理对象使用强引用指针,引用创建的委托方对象,并且成为委托方对象的代理。这就会导致委托方的delegate属性强引用代理对象,导致循环引用的问题,最终两个对象都无法正常释放。设置为弱引用属性。这样在代理对象生命周期存在时,可以正常为我们工作,如果代理对象被释放,委托方和代理对象都不会因为内存释放导致的Crash

    实际运用场景举个例子?

    控制器瘦身:UITableView的数据处理、展示逻辑和简单的逻辑交互都由代理对象去处理,传入一些参数,和控制器相关的逻辑处理传递出来,交由控制器来处理,这样控制器的工作少了很多,而且耦合度也大大降低了。


    2、计算人数工具类

    声明委托方法
    @protocol CountToolDelegate <NSObject>
    
    - (void)willCountAllPerson;// 即将计算人数的委托方法
    - (void)didCountedAllPerson;// 完成计算人数的委托方法
    
    @end
    
    声明数据源方法
    @protocol CountToolDataSource <NSObject>
    
    - (NSArray *)personArray;// 返回包含所有人的数组
    
    @end
    
    声明委托和数据源属性
    @property (nonatomic, weak) id<CountToolDelegate> delegate;// 委托
    @property (nonatomic, weak) id<CountToolDataSource> dataSource;// 数据源
    
    实现计数方法
    - (void)count
    {
        // 调用即将计数的委托方法
        if (self.delegate && [self.delegate conformsToProtocol:@protocol(CountToolDelegate)])
        {
            [self.delegate willCountAllPerson];
        }
        
        // 调用数据源进行计数
        NSArray *persons = [self.dataSource personArray];
        NSLog(@"人数:%@", @(persons.count));
        
        // 调用完成计数的委托方法
        if (self.delegate && [self.delegate respondsToSelector:@selector(didCountedAllPerson)])
        {
            [self.delegate didCountedAllPerson];
        }
    }
    

    3、员工

    声明工号和职位的协议
    @protocol WorkProtocol <NSObject>
    
    @property (nonatomic, strong) NSString *jobNumber;// 工号
    
    @required
    - (void)printJobNumber;// 打印工号
    
    @optional
    - (void)codingAsProgrammer;// 职位
    
    @end
    
    遵从协议并声明协议属性
    @interface Person : NSObject <WorkProtocol>
    
    @property (nonatomic, weak) id<WorkProtocol> delegate;
    
    @end
    
    实现协议方法
    @implementation Person
    
    - (void)printJobNumber
    {
        NSLog(@"打印工号为:%@", self.jobNumber);
    }
    
    - (void)codingAsProgrammer
    {
        NSLog(@"编程者");
    }
    
    @end
    

    4、技术总监

    遵从工具类委托
    @interface Administrator() <CountToolDelegate, CountToolDataSource>
    
    _countTool.delegate = self;
    _countTool.dataSource = self;
    
    实现计算所有人的数目
    - (void)countAllPerson
    {
        [self.countTool count];
    }
    
    CountToolDelegate
    - (void)willCountAllPerson
    {
        NSLog(@"调用了即将计算人数的委托方法");
    }
    
    - (void)didCountedAllPerson
    {
        NSLog(@"调用了完成计算人数的委托方法");
    }
    
    CountToolDataSource
    - (NSArray *)personArray
    {
        return self.allPersons;// 返回包含所有人的数组
    }
    

    5、调用方式

    Person *aPerson = [[Person alloc] init];
    
    // protocol
    aPerson.jobNumber = @"10004847";
    [aPerson printJobNumber];
    [aPerson codingAsProgrammer];
    
    // delegate, dataSource
    Administrator *admin = [[Administrator alloc] init];
    [admin countAllPerson];
    

    输出结果为:

    2020-10-20 17:23:35.042241+0800 DelegateDemo[27449:4938470] 打印工号为:10004847
    2020-10-20 17:23:35.042349+0800 DelegateDemo[27449:4938470] 编程者
    2020-10-20 17:23:35.042450+0800 DelegateDemo[27449:4938470] 调用了即将计算人数的委托方法
    2020-10-20 17:23:35.042539+0800 DelegateDemo[27449:4938470] 人数:2
    2020-10-20 17:23:35.042619+0800 DelegateDemo[27449:4938470] 调用了完成计算人数的委托方法
    

    Demo

    Demo在我的Github上,欢迎下载。
    BasicsDemo

    参考文献

    相关文章

      网友评论

          本文标题:IOS基础使用:Delegate、Protocol

          本文链接:https://www.haomeiwen.com/subject/atyhkltx.html