IOS基础使用:Delegate、Protocol
原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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
解析流程,这些通过代理来展现比较合适。而例如一个网络请求回来,就通过success
、failure
代码块来展示就比较好。
从性能上来说,block
的性能消耗要略大于delegate
,因为block
会涉及到栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理。而代理只是定义了一个方法列表,在运行时向遵守协议的对象发送消息即可。
2、Delegate、Notification、KVO的优缺点比较
Delegate
优势
- 如果
delegate
中的一个方法没有实现那么就会出现编译警告/错误 - 一个控制器中可以实现多个不同的协议
- 能够接收返回值
- 一对一的通信
缺点: 需要定义很多代码
- 协议定义
-
controller
的delegate
属性 - 实现
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