前言
在先前的的仿写中多次使用到了不同界面的传值,这里作一个总结。
属性传值
属性传值是最常用、最简单的一种控制器间数据传递方式,是通过定义属性并设置值来传递数据的。
以在3gshare关注状态的仿写的内容为例,将要跳转进的视图控制器定义为属性,在每次push时,判断该属性是否已经被初始化,如果没有,则初始化,否则不重复操作初始化,使得push进的界面的状态被保留,而不是反复刷新。
@property(nonatomic, strong) FollowViewController *followVC;
if (!self.followVC) {
self.followVC = [[FollowViewController alloc] init];
self.followVC.title = @"新关注的";
self.followVC.followStatus = self.followStatus;
} else {
self.followVC.followStatus = self.followStatus;
}
[self.navigationController pushViewController:self.followVC animated:YES];
-(void)pressBtn:(UIButton*)button {
NSInteger index = button.tag;
BOOL selected = ![self.followStatus[index] boolValue];
self.followStatus[index] = @(selected);
button.selected = selected;
}
效果不再重复展示。
协议传值
协议传值是通过定义协议和代理方法的方式进行多界面传值,常用于反向传值,也就是从第二个页面把数据回传到第一个页面。实现方法是第二页定义一个协议,第一页遵守并实现协议方法,然后第二页通过代理对象调用协议方法,把数据回去。
这里我们依然以3gshare的仿写中注册界面将账号信息传回给登录界面为例:
- 定义协议和声明代理属性(一般定义在被传值的界面,也就是发送数据的一方)
@protocol InformationDelegate <NSObject>
-(void)setupInformation:(NSMutableDictionary*)dictionary;
@end
@interface RegisterViewControl : UIViewController
@property(nonatomic, weak) id<InformationDelegate> dictDelegate;
@end
- 触发代理方法:也就是调用代理方法,一般在被传值的界面。
NSString *UserName = self.UserNameText.text;
NSString *PassWord = self.PassWordText.text;
NSDictionary *dict = [NSDictionary dictionaryWithObject:PassWord forKey:UserName];
[self.dictionary addEntriesFromDictionary:dict];
//self.dictionary交给代理对象self.dictDelegate执行方法setupInformation
[self.dictDelegate setupInformation:self.dictionary];
[self.navigationController popViewControllerAnimated:YES];
- 实现代理方法(一般在传值的界面):也就是谁想接受传值的数据,就实现这个方法。
#import "LoginViewControl.h"
#import "RegisterViewControl.h"
@interface LoginViewControl ()<UITextFieldDelegate, UITabBarDelegate, InformationDelegate>
@end
@implementation LoginViewControl
-(void)setupInformation:(NSMutableDictionary*)dictionary {
[self.dict addEntriesFromDictionary:dictionary];
}
- 设置代理:这一步最重要也最容易被漏掉,它一般写在传值方,也就是接受数据的页面。
-(void)pressRegister {
RegisterViewControl *registerVC = [[RegisterViewControl alloc] init];
registerVC.dictDelegate = self;
[self.navigationController pushViewController:registerVC animated:YES];
}
具体的效果展示也不再重复。
block传值
block传值跟协议代理传值的思路相似,也用于后面向前面的传值,但它使用代码块进行传值。
- 定义block类型和属性
#import <UIKit/UIKit.h>
typedef void(^SendBlock)(NSString *text);
@interface BViewController : UIViewController
@property(nonatomic, copy) SendBlock send;
@end
typedef void(^SendBlock)(NSString *text)
:
- typedof:定义类型别名
- void(^SendBlock)(NSString *text):无返回值,带NSString参数的block类型
- SendBlock:block名字
这里值得注意的是:属性为什么要用 copy
- 首先我们要知道block的存储位置,block 本质上是一个带有上下文的函数对象。根据创建场景不同,block 的存储位置不同:
- 栈上:默认block分配在栈上,随着作用域结束就会被销毁。
- 堆上:如果使用copy关键字,那么系统会将block从栈复制到堆上,这样即使超出作用域,block仍然继续存在。
- 全局区:不捕获外部变量的 block,也叫全局 block,会直接存放在全局区,不需要 copy 就能安全使用。
- 然后我们再来考虑为什么属性要用copy而不是strong。
- strong:赋值时,block可能还是栈上的block,当方法调用完后,栈内存释放,block也随之销毁,但strong修饰的属性还会持有一个悬空指针,访问就会崩溃。
- copy:copy会把 block 拷贝到堆上,生命周期就不依赖原作用域,直到对象释放才销毁,是安全的。
这里我们再用一个代码来看一下区别:
#import <Foundation/Foundation.h>
typedef void(^TestBlock)(void);
@interface MyClass : NSObject
@property(nonatomic, strong) TestBlock strongBlock;
@property(nonatomic, copy) TestBlock copyBlock;
@end
#import <Foundation/Foundation.h>
#import "MyClass.h"
void test(void) {
MyClass *obj = [[MyClass alloc] init];
int value = 42;
obj.strongBlock = ^{
NSLog(@"strongBlock: %d", value);
};
obj.copyBlock = ^{
NSLog(@"copyBlock: %d", value);
};
NSLog(@"strongBlock class: %@", [obj.strongBlock class]);
NSLog(@"copyBlock class: %@", [obj.copyBlock class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
}
return 0;
}
运行结果:
我们发现运行结果和我们预想的不一样,无论strong还是copy,block都在堆上。我查阅了一下资料,发现这是编译器ARC模式下,系统自动帮我们做了一次copy操作,因此我们会发现block存储位置是一样的。然而在MRC模式下的输出结果是我们预想的那样。
这里有些超出我现学的知识,后续了解学习后来完善补充这里的内容。
- 触发block:将要回传的数据作为block的参数传入并执行block。
- (void)backAndSend {
if (self.send) {
self.send(self.textField.text);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
将输入框输入的内容作为参数传入block时,前一个视图控制器接收的信息就会是我传进去的参数。
- 接收数据:在需要接受的地方将传回的值进行使用。
- (void)goToB {
BViewController *bVC = [[BViewController alloc] init];
bVC.send = ^(NSString *text) {
self.label.text = [NSString stringWithFormat:@"收到: %@", text];
};
[self presentViewController:bVC animated:YES completion:nil];
}
展示一下效果:
通知传值
通知传值是一种适合一对多传值的传值方式,它使用了通知中心来实现观察者模式,允许一个对象在发生改变时通知其他观察者对象。
首先我们确定我们的目的,从A跳转到B,并且点击B中按钮将我们字典中存入的内容返回给A,也就是在B界面修改并传值给A界面。
- 发送通知:
将B界面的值传回给A界面,因此在B界面发送通知。
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyNotification"
object:nil
userInfo:info];
- postNotificationName:通知的名称,通过该名称区分不同的通知,以便接收时,监视相应名称的通知。
- object:通知的发送者,表示是哪个对象发送了这个通知。通常情况下,我们不需要传递发送者,可以传入nil。
- userInfo:通知的附加信息,可以通过字典传递一些额外的信息给接收通知的对象。通常情况下,我们在发送通知时,将一些需要传递的数据放入这个字典中。
- 注册监听,注册观察者:
B界面将值传回给A界面,因此在A界面注册监听,也就是A界面愿意接受B界面传回的值。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receiveNotice:)
name:@"MyNotification"
object:nil];
- addObserver:要注册的观察者对象,通常当前对象作为参数接受通知。
- selector:观察者对象用于处理通知的方法,这个方法必须带有一个参数,通常是 NSNotification 对象,用于接收传递的通知信息。
- name:要观察的通知名称,与通知名称匹配。
- object:通知发送者的对象。如果设置为 nil,则会接收任何发送给指定名称的通知。如果设置为特定对象,只有该对象发送的与指定名称匹配的通知才会被发送给观察者。
- 接收通知:
在A界面接收到传回来的值,也就是这个字典,并修改label
- (void)receiveNotice:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
NSString *msg = info[@"message"];
self.label.text = msg;
}
- 移除通知:
最重要且容易遗漏的一步,整个通知传值结束后,一定要移除通知,避免内存泄露
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
效果展示:
KVO传值
KVO是观察者模式在iOS中的实现,也就是一个对象可以监听另一个对象某个属性的变化,当被监听的属性值发生变化时,会收到通知并执行相应的操作。
- 注册观察者
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
- addObserver:观察者对象,即要接受属性变化通知的对象,通常是当前控制器
- forKeyPath:要监听的属性名,用字符串表示
- options:枚举值,指定监听选项,使用(|)连接*
- NSKeyValueObservingOptionNew:新的属性值
- NSKeyValueObservingOptionOld:旧的属性值
- NSKeyValueObservingOptionInitial:注册观察者前,立即发送一次通知,尽管属性没有变化,回调中的change提供当前属性值作为新值
- NSKeyValueObservingOptionPrior:在属性值真正改变之前,先提前发一次通知,回调提供的是变化前的旧值,为改变做准备或清理
- context:指针类型的参数,用于传递额外的上下文信息,通常情况下传入NULL
- KVO回调,实现监听方法
//KVO回调,实现监听方法,当属性改变时调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSNumber *newAge = change[NSKeyValueChangeNewKey];
self.ageLabel.text = [NSString stringWithFormat:@"观察到 Age: %@", newAge];
}
}
- 移除观察者:当观察者对象不再需要监听属性变化时,移除观察者,避免潜在的内存泄漏
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
展示一下效果:
可以看的出,通过KVO传值,实现了两个界面之间类似于3gshare仿写中点赞同步的传值。
这里我们再学习一下KVO的自动触发与手动触发。
- 自动触发:常用于监听对象属性的变化(例如字符串、数字等)。例如上面demo中的age,该属性值发生变化时,KVO会自动发送通知给观察者;使用@synthesize合成属性时,如果指定了观察者,则合成的属性会自动触发KVO通知。
- 手动触发:
- 用于监听集合属性的变化,集合属性不支持自动触发KVO通知。修改集合属性之前调用
willChangeValueForKey:
方法,在修改完成后调用didChangeValueForKey:
方法。(这两个方法必须配套出现) - 用于监听非对象类型(例如C语言基本数据类型)属性,由于它们不是对象,因此无法自动触发KVO通知。
- 用于监听集合属性的变化,集合属性不支持自动触发KVO通知。修改集合属性之前调用
这里展示一个demo:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic, strong) NSMutableArray *friends;
-(void)addFriends:(NSString *)name;
-(void)removeFriends:(NSString *)name;
@end
-(void)addFriends:(NSString *)name {
[self willChangeValueForKey:@"friends"];
[self.friends addObject:name];
[self didChangeValueForKey:@"friends"];
}
-(void)removeFriends:(NSString *)name {
[self willChangeValueForKey:@"friends"];
[self.friends removeObject:name];
[self didChangeValueForKey:@"friends"];
}
!非常重要的:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"friends"]) {
return NO;//关闭自动触发
}
return [super automaticallyNotifiesObserversForKey:key];
}
这是KVO通知开关,这个方法必须有,因为只有关闭自动触发后才会调用willChangeValueForKey:
、didChangeValueForKey:
两个方法,否则观察者不会收到任何通知,程序就会报错。
// 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"friends"]) {
NSLog(@"friends 改变了 old=%@ new=%@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
NSLog(@"当前好友列表:%@", self.person.friends);
}
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"friends"];
}
输出结果:
我们会发现点击删除时输出均为nil,这是因为willChangeValueForKey:
、didChangeValueForKey:
这两个方法实际是告诉程序属性将要改变了,属性已经改变了,但是不会知道数组中具体增加了还是删除了具体哪个元素。为解决这个问题,willChange:valuesAtIndexes:forKey:
、didChange:valuesAtIndexes:forKey:
这两个方法可以找到数组里具体哪个元素,这样change中便有了具体值。
总结
在仿写项目的过程中,多界面传值是很重要的部分,笔者会在学习后继续补充完善。