欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS----KVC和KVO 详解

程序员文章站 2022-08-03 10:09:51
一、简述 在iOS开发过程中,页面跳转时在页面之间进行数据传递是很常见的事情,我们称这个过程为页面传值。页面跳转过程中,从主页面跳转到子页面的数据传递称之为正向传值;反之,从子页面返回主页面时的数据传递称之为反向传值。 目前我所了解和掌握的传值方式有: 二、页面传值的详解 2.0 准备工作 为了实现 ......

一、简述

  在ios开发过程中,页面跳转时在页面之间进行数据传递是很常见的事情,我们称这个过程为页面传值。页面跳转过程中,从主页面跳转到子页面的数据传递称之为正向传值;反之,从子页面返回主页面时的数据传递称之为反向传值

  目前我所了解和掌握的传值方式有:

  1. 性传值
  2. 单例传值
  3. nsuserdefaults传值
  4. 代理传值
  5. block传值
  6. 通知传值
  7. kvo/kvc  ios----kvc和kvo 详解

二、页面传值的详解

2.0 准备工作

  为了实现页面之间传值,我们需要准备两个页面,代码结构如下图所示。其中,klmainviewcontroller为主主页面,klsubviewcontroller为子页面,页面之间的跳转使用uinavigationcontroller来实现。每个页面中都有一个文本编辑框,我们需要将其中一个页面文本框中的内容传递到另一个页面中。

iOS----KVC和KVO 详解     iOS----KVC和KVO 详解    iOS----KVC和KVO 详解

iOS----KVC和KVO 详解
 1 #import "klmainviewcontroller.h"
 2 #import "klsubviewcontroller.h"
 3 
 4 @interface klmainviewcontroller ()
 5 
 6 @property (strong, nonatomic) uitextfield *textfield;
 7 @property (strong, nonatomic) uibutton *button;
 8 
 9 @end
10 
11 @implementation klmainviewcontroller
12 
13 - (void)viewdidload {
14     [super viewdidload];
15     self.title = @"主界面";
16     
17     _textfield = [[uitextfield alloc] init];
18     _textfield.textcolor = [uicolor redcolor];
19     _textfield.textalignment = nstextalignmentcenter;
20     _textfield.backgroundcolor = kbgcolor;
21     _textfield.text = @"主界面的label信息";
22     [self.view addsubview:_textfield];
23     weakself
24     [_textfield mas_makeconstraints:^(masconstraintmaker *make) {
25         make.center.mas_equalto(weakself.view).mas_offset(0.0f);
26         make.left.mas_equalto(weakself.view).mas_offset(15.0f);
27         make.right.mas_equalto(weakself.view).mas_offset(-15.0f);
28     }];
29     
30     _button = [uibutton buttonwithtype:uibuttontypecustom];
31     [_button settitle:@"跳转到子界面" forstate:uicontrolstatenormal];
32     [_button settitlecolor:[uicolor bluecolor] forstate:uicontrolstatenormal];
33     [_button addtarget:self action:@selector(btnclicked:) forcontrolevents:uicontroleventtouchupinside];
34     [self.view addsubview:_button];
35     [_button mas_makeconstraints:^(masconstraintmaker *make) {
36         make.centerx.mas_equalto(weakself.view).mas_offset(0.0f);
37         make.top.mas_equalto(weakself.textfield.mas_bottom).mas_offset(40.0f);
38     }];
39     
40 }
41 
42 - (void) btnclicked:(uibutton *)btn {
43     klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
44     [self.navigationcontroller pushviewcontroller:subvc animated:yes];
45 }
46 
47 @end
klmainviewcontroller.m
iOS----KVC和KVO 详解
 1 //klsubviewcontroller.h
 2 #import <uikit/uikit.h>
 3 
 4 ns_assume_nonnull_begin
 5 
 6 @interface klsubviewcontroller : uiviewcontroller
 7 
 8 @property (strong, nonatomic) uitextfield *textfield;
 9 @property (strong, nonatomic) uibutton *button;
10 
11 @property (strong, nonatomic) nsstring *content;
12 
13 @end
14 
15 ns_assume_nonnull_end
16 
17 //klsubviewcontroller.m
18 #import "klsubviewcontroller.h"
19 
20 @interface klsubviewcontroller ()
21 
22 @end
23 
24 @implementation klsubviewcontroller
25 
26 - (void)viewdidload {
27     [super viewdidload];
28     self.view.backgroundcolor = [uicolor whitecolor];
29     self.title = @"子界面";
30     
31     _textfield = [[uitextfield alloc] init];
32     _textfield.textcolor = [uicolor redcolor];
33     _textfield.textalignment = nstextalignmentcenter;
34     _textfield.backgroundcolor = kbgcolor;
35     _textfield.text = @"子界面的label信息";
36     [self.view addsubview:_textfield];
37     weakself
38     [_textfield mas_makeconstraints:^(masconstraintmaker *make) {
39         make.center.mas_equalto(weakself.view).mas_offset(0.0f);
40         make.left.mas_equalto(weakself.view).mas_offset(15.0f);
41         make.right.mas_equalto(weakself.view).mas_offset(-15.0f);
42     }];
43     
44     _button = [uibutton buttonwithtype:uibuttontypecustom];
45     [_button settitle:@"返回主界面" forstate:uicontrolstatenormal];
46     [_button settitlecolor:[uicolor bluecolor] forstate:uicontrolstatenormal];
47     [_button addtarget:self action:@selector(btnclicked:) forcontrolevents:uicontroleventtouchupinside];
48     [self.view addsubview:_button];
49     [_button mas_makeconstraints:^(masconstraintmaker *make) {
50         make.centerx.mas_equalto(weakself.view).mas_offset(0.0f);
51         make.top.mas_equalto(weakself.textfield.mas_bottom).mas_offset(40.0f);
52     }];
53 }
54 
55 - (void) btnclicked:(uibutton *)btn {
56     
57     [self.navigationcontroller popviewcontrolleranimated:yes];
58 }
59 
60 @end
klsubviewcontroller

2.1 属性传值

方法描述:在从当前页面跳转到下主页面之前,提前创建下主页面,通过赋值的方式将当前页面的数据赋予下主页面的属性。

适用场景:当从主页面push到子页面时,子页面需要使用到主页面的数据,我们需要使用到正向传值。

传递方式:正向传值。

使用步骤

  1. 子页面的.h文件中定义属性来保留要传递过来的数据
    //子页面klsubviewcontroller.h的属性定义
    @interface klsubviewcontroller : uiviewcontroller
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @property (strong, nonatomic) nsstring *content;//属性接收数据
    
    @end
  2. 主页面在跳转的时候将数据赋值给子页面对应的属性 
    //主界面跳转时将数据赋值给对应的属性
    @interface klmainviewcontroller ()
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        ......   
    }
    
    //跳转
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        subvc.content = @"来自主界面的数据";
    //    subvc.textfield.text = @"来自主界面的数据"; //这样传递是有问题的,因为子页面中的textfield是在viewdidload中进行初始化和布局的,在这时候textfield还没有初始化,为nil,所以赋值是失效的
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    }
    
    @end

2.2 代理传值

方法描述:首先在子页面的头文件中添加一个代理(协议)的定义,定义一个传递数据的方法,并且在子页面的类中添加一个代理属性;然后,在子页面返回主页面之前调用代理中定义的数据传递方法(方法参数就是要传递的数据);最后,在主页面中遵从该代理,并实现代理中定义的方法,在方法的实现代码中将参数传递给主页面的属性。

适用场景:已经通过push的方式进入到子页面,在从子页面返回主页面的时候(子页面会释放掉内存),需要在主页面中使用子页面中的数据,这是就可以利用代理反向传值。

传递方式:反向传值。

使用步骤

  1. 在子页面中添加一个代理协议,在协议中定义一个传递数据的方法
  2. 在子页面.h文件中添加一个代理属性
    //子页面的.h文件,定义代理以及代理属性
    // 声明代理
    @protocol btoadelegate <nsobject>
    // 代理方法
    - (void)transferstring:(nsstring *)string;
    @end
    
    @interface klsubviewcontroller : uiviewcontroller
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @property (nonatomic, weak) id<btoadelegate> delegate;//代理属性
    
    @end
  3. 在子页面返回主页面之前掉好用代理中定义数据传递方法,方法参数就是要传递的数据
    //子页面返回时调用代理方法
    - (void) btnclicked:(uibutton *)btn {
        //如果当前的代理存在,并且实现了代理方法,则调用代理方法进行传递数据
        if (self.delegate &&
            [self.delegate respondstoselector:@selector(transferstring:)]) {
            [self.delegate transferstring:@"子页面回传的数据"];
        }
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }
  4. 在主页面中遵从该代理,并实现代理中定义的方法,在方法的实现代码中将参数传递给主页面的属性
    //要实现btoadelegate
    @interface klmainviewcontroller () <btoadelegate>
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        ... 
    }
    
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        subvc.delegate = self; //申明子页面的代理是主页面自身self
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    }
    
    #pragma mark btoadelegate 代理方法,子页面调用的时候会回调该方法
    - (void)transferstring:(nsstring *)string {
        self.textfield.text = string;
    }
    
    @end

 2.3 block传值

方法描述:在子页面中添加一个块语句属性,在子页面返回主页面之前调用该块语句。在主页面跳转子页面之前,设置子页面中的块语句属性将要执行的动作(回调函数)。这样,在子页面返回主页面时就会调用该回调函数来传递数据。

适用场景:已经通过push的方式进入到子页面,在从子页面返回主页面的时候(子页面会释放掉内存),需要在主页面中使用子页面中的数据,这是就可以利用代理反向传值。

传递方式:反向传递。

使用步骤:整个步骤和代理差不多

  1. 在子页面中添加一个块语句属性
    //定义block的类型
    typedef void (^transdatablock)(nsstring *content);
    
    @interface klsubviewcontroller : uiviewcontroller
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @property (copy, nonatomic) transdatablock transdatablock;//定义一个block属性,用于回传数据
    
    @end
  2. 在子页面返回主页面之前调用该块语句
    #import "klsubviewcontroller.h"
    
    @interface klsubviewcontroller ()
    
    @end
    
    @implementation klsubviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.view.backgroundcolor = [uicolor whitecolor];
        self.title = @"子界面";
        
        //布局代码省略
        ......
    }
    
    - (void) btnclicked:(uibutton *)btn {
        //如果回传block存在 则调用该block进行回传数据
        if (self.transdatablock) {
            self.transdatablock(@"子页面回传的数据");
        }
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }
    
    @end
  3. 在主页面跳转子页面之前,设置子页面中的块语句属性将要执行的动作(回调函数)
    #import "klmainviewcontroller.h"
    #import "klsubviewcontroller.h"
    
    @interface klmainviewcontroller ()
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        ......
    }
    
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        //通过子页面的block回传拿到数据后进行处理,赋值给当前页面的textfield
        subvc.transdatablock = ^(nsstring *content) {
            self.textfield.text = content;
        };
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    }
    
    @end 

2.4 通知传值

方法描述:在通知接收方需要注册通知,并指定接收到通知后进行的操作;而在通知发送方则在需要传递数据时发送通知就ok了。通知的操作都是通过nsnotificationcenter来完成的。

但是要注意的两点是:

  • 要想能够接收到通知进行处理,必须先注册通知。
  • 在注册通知的页面消毁时一定要移除已经注册的通知,否则会造成内存泄漏
  • 注册的接收通知的名称必须和发送通知的名称保持一致才能接收到,否则无法接收到发出的通知

适用场景

  • 一般用于已经通过push的方式进入到子页面,在从子页面返回主页面的时候(子页面会释放掉内存),需要在主页面中使用子页面中的数据,这是就可以利用通知反向传值。
  • 但是也可以用于通过push进入子页面时向子页面传递数据,这时就可以用通知进行正向传值。

传递方式:正向传递(很少这样用)、反向传递(更常用)。

使用步骤

  • 反向传递:
  1. 在子页面返回的时候发送通知,注册的接收通知的名称必须和发送通知的名称保持一致才能接收到,否则无法接收到发出的通知
    @interface klsubviewcontroller ()
    
    @end
    
    @implementation klsubviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.view.backgroundcolor = [uicolor whitecolor];
        self.title = @"子界面";
        
        //布局代码省略
        .......
    }
    
    - (void) btnclicked:(uibutton *)btn {
        //发送通知回传数据,回传的数据格式自定义,这里定义为dictionary类型
        [[nsnotificationcenter defaultcenter] postnotificationname:@"transdatanoti" object:nil userinfo:@{@"content":@"子页面回传的数据"}];
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }
  2. 在主页面注册通知,并制定接收到通知后执行的操作方法。需要注意的是,在注册通知的页面消毁时一定要移除已经注册的通知,否则会造成内存泄漏。
    @interface klmainviewcontroller ()
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)dealloc {
        //移除所有通知
        [[nsnotificationcenter defaultcenter] removeobserver:self];
        // 移除某个
        // [[nsnotificationcenter defaultcenter] removeobserver:self name:@"transdatanoti" object:nil];
    }
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        ......
        
        //注册通知,用于接收通知,接收通知的名称必须和发送通知的名称保持一致才能接收到,否则无法接收到发出的通知
        [[nsnotificationcenter defaultcenter] addobserver:self selector:@selector(notireceived:) name:@"transdatanoti" object:nil];
    }
    
    //接收通知,解析内容进行处理
    - (void)notireceived:(nsnotification *)sender {
        self.textfield.text = sender.userinfo[@"content"];
    }
    
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    }
    
    @end
  •  正向传递:和反向传递的不走基本就是反过来就ok了,但是有一点需要注意的是正向传递时从主界面push到子界面时发送通知,这时候要确保子界面已经注册了通知,否则会收不到通知的,所以正向传递时,子界面通知的注册应该在子界面的初始化init方法中进行。
  1. 在主页面返回的时候发送通知,注册的接收通知的名称必须和发送通知的名称保持一致才能接收到,否则无法接收到发出的通知。
    @interface klmainviewcontroller ()
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        ......
    }
    
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        //发送通知回传数据,回传的数据格式自定义,这里定义为dictionary类型
        [[nsnotificationcenter defaultcenter] postnotificationname:@"transdatanoti" object:nil userinfo:@{@"content":@"主页面传递的数据"}];
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    }
    
    @end
  2. 在子页面注册通知,并制定接收到通知后执行的操作方法。正向传递时注册通知、基本布局不能放在viewdidload中,要放在初始化函数init中。
    @interface klsubviewcontroller ()
    
    @end
    
    @implementation klsubviewcontroller
    
    - (void)dealloc {
        //移除所有通知
        [[nsnotificationcenter defaultcenter] removeobserver:self];
        // 移除某个
        // [[nsnotificationcenter defaultcenter] removeobserver:self name:@"transdatanoti" object:nil];
    }
    
    - (instancetype)init {
        self = [super init];
        
        //初始化代码省略
        ......
        
        //注册通知,用于接收通知,接收通知的名称必须和发送通知的名称保持一致才能接收到,否则无法接收到发出的通知
        [[nsnotificationcenter defaultcenter] addobserver:self selector:@selector(notireceived:) name:@"transdatanoti" object:nil];
        return self;
    }
    
    - (void)viewdidload {
        [super viewdidload];
        self.view.backgroundcolor = [uicolor whitecolor];
        self.title = @"子界面";
    }
    
    //接收通知,解析内容进行处理
    - (void)notireceived:(nsnotification *)sender {
        self.textfield.text = sender.userinfo[@"content"];
    }
    
    - (void) btnclicked:(uibutton *)btn {
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }
    
    @end

2.5 nsuserdefaults传值

方法描述:nsuserdefaults传值是将所要传的值写在沙盒目录里面,需要获取值的时候直接访问沙盒,获取这个值就可以了,这种传值方法一般用在需要将数据本地存储的时候,比如:用户名之类,当用户下次登录或者使用app的时候,可以直接从本地读取。

适用场景:任何需要数据传递的场景都适用,但是传递数据的类型仅限于基本数据类型,不能用于自定义的对象类型。

传递方式:正向传值、反向传值。

使用步骤

  1. 需要传值时将数据通过nsuserdefaults保存到沙盒目录里面
    - (void) btnclicked:(uibutton *)btn {
        /*
         setobject:后面写的就是所需要传递的值
         forkey:要具有唯一性、一致性;
         唯一性是指:当代码中用到多个nsuserdefaults方法时,要保证不同的key不一样,否则就是覆盖值
         一致性:这里传递一个值,当需要用到的时候,要用valueforkey的方法,这个key和传值时候写的key要一样,写错了就找不到值了。
         */
        [[nsuserdefaults standarduserdefaults] setobject:@"nsuserdefaults传值" forkey:@"nsuserdefaults"];
        [[nsuserdefaults standarduserdefaults] synchronize];
        
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }
  2. 需要使用值时通过nsuserdefaults从沙盒目录里面取值进行处理
    _label.text = [[nsuserdefaults standarduserdefaults] valueforkey:@"nsuserdefaults"];

2.6 单例传值

方法描述:单例传值的性质和nsuserdefaults传值的性质类似,只是单例传值是将数据保存在单例对象中,需要的时候同样从单例对象中去获取数据使用就ok。

适用场景:任何需要数据传递的场景都适用,传递的数据可以是任何类型的数据。

传递方式:正向传值、反向传值均ok。

使用步骤

  1. 创建一个类,拥有一些属性用于保存数据,并实现单例方法
    @interface kldanliobj : nsobject
    
    @property (nonatomic, copy) nsstring *content; //保存数据数据的属性
    
    + (instancetype) sharddanliobj; //单例对象获取方法
    
    @end
    #import "kldanliobj.h"
    
    static danli *danli = nil;
    
    @implementation kldanliobj
    
    + (instancetype) sharddanliobj {
        //实现方法,判断是否为空,是就创建一个全局实例给它
        if (danli == nil) {
            danli = [[kldanliobj alloc] init];
        }
        return danli;
    }
    
    @end
  2. 需要传递数据时使用单例类将数据保存到单例的属性中
    [kldanliobj sharddanliobj].content = @"主界面传递的数据"; 
  3. 需要使用值时通过单例的属性获取数据进行使用和处理
    self.textfield.text = [kldanliobj sharddanliobj].content; 

2.7 kvc传值

方法描述:kvc(key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指ios的开发中,可以允许开发者通过key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法,这样就可以在运行时动态地访问和修改对象的属性。这其实和属性传值比较类似。

适用场景:当从主页面push到子页面时,子页面需要使用到主页面的数据,我们需要使用到正向传值。

传递方式:正向传值

使用步骤

  1. 在需要传值时使用kvc给子页面的属性进行赋值就ok了
    - (void) btnclicked:(uibutton *)btn {
        klsubviewcontroller *subvc = [[klsubviewcontroller alloc] init];
        //给子页面subvc的属性content赋值 和subvc.content = @"主页面传递的数据";效果一样
        [subvc setvalue:@"主页面传递的数据" forkey:@"content"];
        [self.navigationcontroller pushviewcontroller:subvc animated:yes];
    } 

2.8 kvo传值

方法描述:kvo(key-value-observing,键值观察),即观察关键字的值的变化。首先在子页面中声明一个待观察的属性,在返回主页面之前修改该属性的值。在主页面中提前分配并初始化子页面,并且注册对子页面中对应属性的观察者。在从子页面返回上主之前,通过修改观察者属性的值,在主页面中就能自动检测到这个改变,从而读取子页面的数据。

适用场景:已经通过push的方式进入到子页面,在从子页面返回主页面的时候(子页面会释放掉内存),需要在主页面中使用子页面中的数据,这是就可以利用代理反向传值。

传递方式:反向传递。

使用步骤

  1. 在主页面注册观察者,实现kvo的回调方法,并在主页面销毁时移除观察者
    @interface klmainviewcontroller ()
    
    @property (strong, nonatomic) uitextfield *textfield;
    @property (strong, nonatomic) uibutton *button;
    
    @property (strong, nonatomic) klsubviewcontroller *subvc;
    
    @end
    
    @implementation klmainviewcontroller
    
    - (void)dealloc {
        //移除观察者
        [self.subvc removeobserver:self forkeypath:@"content"];
    }
    
    - (void)viewdidload {
        [super viewdidload];
        self.title = @"主界面";
        
        //布局代码省略
        .......
        
    }
    
    - (void) btnclicked:(uibutton *)btn {
        if (!_subvc) {
            _subvc = [[klsubviewcontroller alloc] init];
            //注册观察者
             [_subvc addobserver:self forkeypath:@"content" options:(nskeyvalueobservingoptionnew | nskeyvalueobservingoptionold) context:nil];
        }
        [self.navigationcontroller pushviewcontroller:_subvc animated:yes];
    }
    
    // kvo的回调,当观察者中的数据有变化时会回调该方法
    - (void)observevalueforkeypath:(nsstring *)keypath ofobject:(id)object change:(nsdictionary<nskeyvaluechangekey,id> *)change context:(void *)context{
        if ([keypath isequaltostring:@"content"]){
            self.textfield.text = self.subvc.content;
        }
    }
    
    @end
  2. 子页面在返回主页面时修改对应属性的内容,则会回调主页面的回调方法
    - (void) btnclicked:(uibutton *)btn {
        self.content = @"子页面回传数据";//修改属性的内容
        [self.navigationcontroller popviewcontrolleranimated:yes];
    }