声明:本文绝非原创,笔者只是站在巨人的肩膀上总结网络上各位大神的笔记和博客文章,在此向大神们致敬!
在iOS开发过程中,页面跳转时在页面之间进行数据传递是很常见的事情,我们称这个过程为页面传值。页面跳转过程中,从一级页面跳转到二级页面的数据传递称之为正向传值;反之,从二级页面返回一级页面时的数据传递称之为反向传值。
准备工作
为了实现页面之间传值,我们需要准备两个页面,代码结构如下图所示。其中,MainViewController为一级页面,SubViewController为二级页面,页面之间的跳转使用UINavigationController来实现。
页面之间的跳转动画如下所示,每个页面中都有一个文本编辑框,我们需要将其中一个页面文本框中的内容传递到另一个页面中。
1. 属性传值
方法描述:在从当前页面跳转到下一级页面之前,提前创建下一级页面,通过赋值的方式将当前页面的数据赋予下一级页面的属性。传递方式:正向传值。
适用场景:当从一级页面push到二级页面时,二级页面需要使用到一级页面的数据,我们需要使用到正向传值。
例如,我们首先需要在二级页面中定义一个*NSString text属性,用于保存一级页面传递给下一级页面的数据。在SubViewController.h中定义属性代码如下:
@interface SubViewController : UIViewController
@property (copy,nonatomic) NSString *text;
@property (weak,nonatomic) IBOutlet UITextField *textField;
- (IBAction)backJumpBtnClicked:(id)sender;
@end
然后,需要在一级页面按钮按下时的跳转代码中将当前页面的textField中的内容通过属性赋值的方式传递给二级页面中的text属性。
- (IBAction)mainJumpBtnClicked:(id)sender {
SubViewController *subPage = [[SubViewController alloc]init];
subPage.text = _textField.text;
[self.navigationController pushViewController:subPage animated:YES];
}
然后,需要在SubViewController的viewDidLoad中设置页面中textField的内容:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
_textField.text = _text;
}
注意:如果你使用如下的方式进行属性传值并不能成功。
SubViewController *subPage = [[SubViewController alloc]init];
subPage.textField.text = _textField.text;
[self.navigationController pushViewController:subPage animated:YES];
目前,我也不知道出现这种情况的真正原因,不过分析可能是由于二级页面中的textField是从xib文件到导出的,数据weak类型,可能这个时候还没有分配内存的关系导致了赋值失败。
2. 代理传值
方法描述:首先在二级页面的头文件中添加一个代理(协议)的定义,定义一个传递数据的方法,并且在二级页面的类中添加一个代理属性;然后,在二级页面返回上一级页面之前调用代理中定义的数据传递方法(方法参数就是要传递的数据);最后,在一级页面中遵从该代理,并实现代理中定义的方法,在方法的实现代码中将参数传递给一级页面的属性。
使用场景:已经通过push的方式进入到二级页面,在从二级页面返回一级页面的时候(二级页面会释放掉内存),需要在一级页面中使用二级页面中的数据,这是就可以利用代理反向传值。
第1步:在二级页面的头文件中添加一个代理的定义。
第2步:在二级页面的属性中添加一个代理属性。二级页面的头文件如下:
#import <UIKit/UIKit.h>
// 声明代理
@protocol SubToMainDelegate <NSObject>
// 代理方法
- (void)transferData:(NSString*)text;
@end
@interface SubViewController : UIViewController
@property (weak,nonatomic) IBOutlet UITextField *textField;
- (IBAction)backJumpBtnClicked:(id)sender;
// 代理属性
@property (weak,nonatomic) id<SubToMainDelegate> delegate;
@end
第3步:在二级页面消失之前,调用数据传递的代理方法,通过该方法将二级页面中的数据传递给实现了该代理方法的对象。二级页面的实现文件代码如下:
#import "SubViewController.h"
@interface SubViewController ()
@end
@implementation SubViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)backJumpBtnClicked:(id)sender {
// 判断有没有代理以及代理是否响应代理方法
if (self.delegate &&
[self.delegate respondsToSelector:@selector(transferData:)]) {
[self.delegate transferData:self.textField.text];
}
[self.navigationController popViewControllerAnimated:YES];
}
@end
第4步:在一级页面中遵从二级页面中定义的协议。
第5步:实现代理中的数据传递方法。一级页面的实现代码如下:
#import "MainViewController.h"
#import "SubViewController.h"
@interface MainViewController ()<SubToMainDelegate>
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)mainJumpBtnClicked:(id)sender {
SubViewController *subPage = [[SubViewController alloc]init];
subPage.delegate = self;
[self.navigationController pushViewController:subPage animated:YES];
}
// MARK: 实现SubToMainDelegate中的代理方法
- (void)transferData:(NSString *)text{
self.textField.text = text;
}
@end
注意: 从动画可以看出,我们通过点击二级页面的返回上一级按钮时,可以实现反向传值。但是,如果直接点击导航栏的Back键,并不能实现传值效果,这是因为导航栏的返回按钮没有实现代理中的方法,解决办法是添加一个自定义按钮代替导航栏左侧的返回按钮,并且将新添加的按钮selector设置为返回上一级按钮的方法。
3. Block传值
方法描述:在二级页面中添加一个块语句属性,在二级页面返回一级页面之前调用该块语句。在一级页面跳转二级页面之前,设置二级页面中的块语句属性将要执行的动作(回调函数)。这样,在二级页面返回一级页面时就会调用该回调函数来传递数据。
适用场景:反向传值。
第1步:在二级页面的头文件中定义一个Block属性,代码如下:
#import <UIKit/UIKit.h>
// 定义一个Block
typedef void(^SubToMainBlock)(NSString *text);
@interface SubViewController : UIViewController
@property (weak,nonatomic) IBOutlet UITextField *textField;
- (IBAction)backJumpBtnClicked:(id)sender;
// 添加一个Block属性
@property (copy,nonatomic) SubToMainBlock data;
@end
第2步:在点击返回上一级按钮的事件处理代码中调用块语句传值,二级页面的实现文件代码内容如下:
#import "SubViewController.h"
@interface SubViewController ()
@end
@implementation SubViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)backJumpBtnClicked:(id)sender {
// Block传值
_data(self.textField.text);
[self.navigationController popViewControllerAnimated:YES];
}
@end
第3步:在一级页面跳转二级页面之前,设置在二级页面执行的块语句回调函数,代码如下:
#import "MainViewController.h"
#import "SubViewController.h"
@interface MainViewController ()
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)mainJumpBtnClicked:(id)sender {
SubViewController *subPage = [[SubViewController alloc]init];
__weak typeof(self) mainPtr = self;
// Block回调接收数据
[subPage setData:^(NSString *text){
mainPtr.textField.text = text;
}];
[self.navigationController pushViewController:subPage animated:YES];
}
@end
注: 程序的运行效果与方法2代理传值的运行效果相同。
4. KVO传值
方法描述:KVO(Key-Value-Observing,键值观察),即观察关键字的值的变化。首先在二级页面中声明一个待观察的属性,在返回一级页面之前修改该属性的值。在一级页面中提前分配并初始化二级页面,并且注册对二级页面中对应属性的观察者。在从二级页面返回上一级之前,通过修改观察者属性的值,在一级页面中就能自动检测到这个改变,从而读取二级页面的数据。
适用场景:反向传值。
KVO使用三大步:
(1) 注册观察者
(2) KVO的回调
(3) 移除观察者
以上三大步都在一级页面中实现,代码如下:
#import "MainViewController.h"
#import "SubViewController.h"
@interface MainViewController ()
@property (strong,nonatomic) SubViewController *subPage;
@end
@implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)mainJumpBtnClicked:(id)sender {
// 懒加载
if (_subPage == nil){
_subPage = [[SubViewController alloc]init];
// 注册观察者
[_subPage addObserver:self forKeyPath:@"data" options:NSKeyValueObservingOptionNew context:nil];
}
[self.navigationController pushViewController:_subPage animated:YES];
}
// KVO的回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"data"]){
self.textField.text = _subPage.data;
}
}
// 移除KVO
- (void)dealloc{
[_subPage removeObserver:self forKeyPath:@"data"];
}
注册观察者中的forKeyPath参数用于指定需要观察的二级页面中的属性名称。
说明:代码中对于二级页面使用了懒加载的方式,创建了该页面之后,返回一级页面时此页面并不会释放内存,因此,下一次进入二级页面时数据保持不变。
首先,在二级页面中生命我们需要在一级页面中观察的属性“data”:
#import <UIKit/UIKit.h>
@interface SubViewController : UIViewController
@property (weak,nonatomic) IBOutlet UITextField *textField;
- (IBAction)backJumpBtnClicked:(id)sender;
// 添加一个观察的属性
@property (copy,nonatomic) NSString *data;
@end
然后,在二级页面返回上一级之前,修改在一级页面中观察的属性的值:
#import "SubViewController.h"
@interface SubViewController ()
@end
@implementation SubViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)backJumpBtnClicked:(id)sender {
// 修改属性的值,在一级页面中监听该属性
self.data = self.textField.text;
[self.navigationController popViewControllerAnimated:YES];
}
@end
小心陷阱:在修改观察的属性时,不能使用简写_data = self.textField.text;虽然这种写法在我们平时的使用中可以与self.data等效,但是在KVO中如果使用_data来修改data属性的值,一级页面并不能检测到这种改变。因此,必须使用完整的self.data形式来修改data属性的值。
注意:观察者的注册和移除要对应,如果移除时发现没有注册观察者,程序会crash。
5. 通知传值
方法描述:
适用场景:正向传值、反向传值。
6. 单例传值
方法描述:
适用场景:正向传值、反向传值。
7. KVC传值
方法描述:
适用场景:正向传值。
总结:本文介绍了iOS中页面传值的常用方法,归纳主要分为属性传值、代理传值、Block传值、KVO传值、通知传值、单例传值和KVC传值这7种方法。文中以示例的方式介绍了每种方法的用法和使用场景,以便开发者在产品开发过程中选择和参考。