KVC 键值编码

1、KVC

  • KVC 是 Key-Value Coding 的简写,是键值编码的意思,属于 runtime 方法。Key Value Coding 是 cocoa 的一个标准组成部分,是间接给对象属性设置数值的方法,它能让我们可以通过 name(key) 的方式访问属性变量, 不必调用明确的属性访问方法, 如我们有个属性变量叫做 foo, 我们可以 foo 直接访问它,同样我们也可以用 KVC 来完成 [Object valueForKey:@“foo”], 这样做主要的好处就是来减少我们的代码量。

  • 程序执行过程中,KVC 动态给对象属性设置数值,不关心属性在 .h 和 .m 中是如何定义的,只要对象有属性,就能够读取和设置。这种方式,有点违背程序的开发原则。

  • 在 iOS 中,用 KVC 用的最多是核心动画,核心动画是通过 KVC 对涂层的可动画属性设置数值来实现的。

2、数据模型

  • 模型是专门用来存放数据的对象,一般都是一些直接继承自 NSObject 的纯对象,内部会提供一些属性来存放数据,控制器可以直接传递模型给视图控件以显示空间的内容。

  • 1)用模型取代字典的好处

    • 使用字典的坏处:

      • 一般情况下,设置数据和取出数据都使用 “字符串类型的 key”,编写这些 key 时,编辑器没有智能提示,需要手敲,手敲字符串 key,key 容易写错,Key 如果写错了,编译器不会有任何警告和报错,造成设错数据或者取错数据。如:

        1
        2
        dict[@"name"] = @"Jack";
        NSString *name = dict[@"name"];
    • 使用模型的好处:

      • 所谓模型,其实就是数据模型,专门用来存放数据的对象,用它来表示数据会更加专业。模型设置数据和取出数据都是通过它的属性,属性名如果写错了,编译器会马上报错,因此,保证了数据的正确性
        使用模型访问属性时,编译器会提供一系列的提示,提高编码效率。

        1
        2
        app.name = @"Jack";
        NSString *name = app.name;
  • 2)字典转模型

    • 字典转模型的过程最好封装在模型内部。

    • 模型应该提供一个可以传入字典参数的构造方法。

      1
      2
      - (instancetype)initWithDict:(NSDictionary *)dict;
      + (instancetype)xxxWithDict:(NSDictionary *)dict;
  • 3)字典转模型 KVC 方法

    • 字典转模型:setValuesForKeysWithDictionary
      • 字典中的 key 值需与要赋值的对象的属性变量名相同,并且都为字符串类型。
    • 模型转字典:dictionaryWithValuesForKeys
      • 参数是要被转换到字典中的属性名称
  • 4)KVC 数据模型的设置

    • 为了避免服务端返回的数值型数据是 null,可以把数值型的数据设置成 NSNumber 类型,否则会报错 could not set nil as the value for the key messageId 。

    • id 是服务端最喜欢用的属性,id 在 iOS 中是关键字,但在模型中可以正常使用的。

    • copy 属性,在设置数值的时候,如果有一方是可变的,会默认做一次 copy 操作,会建立新的副本,在模型中对象全都是用 copy 属性会比较安全。

    • 定义为 copy 的属性,重写了 setter 方法之后,定义属性的 copy 就是摆设了,不会默认进行 copy 操作,必须要自己 copy 一下,否则设置数值的时候,不会 copy。
  • 5)字典转模型的过程

3、KVC 赋值与取值

  • KVC 是一种操作全局变量的方法,无论是公有的,私有的,还是受保护的全都可以操作。

    • 1、找对象的 setter 方法,找到就执行;
    • 2、找不到 setter 方法就找 _name 变量,找到就赋值;
    • 3、如果找不到 _name 变量,就找 name;
    • 4、如果 name 也找不到就会让对象调用 -(void)setValue:forUnderfinedKey; 方法处理异常。
  • Objective-C

    • KvcClass.h

      1
      2
      3
      4
      5
      6
      @property(nonatomic, assign) NSInteger ID;

      @property(nonatomic, copy) NSString *name;
      @property(nonatomic, assign) NSInteger age;

      @property(nonatomic, retain) SubKvcClass *subKVC;
    • SubKvcClass.h

      1
      2
      @property(nonatomic, copy) NSString *subName;
      @property(nonatomic, assign) NSInteger subAge;
    • ViewController.m

      1
      2
      3
      4
      KvcClass *kvcObject = [[KvcClass alloc] init];

      SubKvcClass *subKVCObject = [[SubKvcClass alloc] init];
      kvcObject.subKVC = subKVCObject;
  • Swift

    • KvcClass.swift

      1
      2
      3
      4
      5
      6
      var ID: NSInteger!

      var name: String!
      var age: NSInteger = 0

      var subKVC: SubKvcClass!
    • SubKvcClass.swift

      1
      2
      var subName: String!
      var subAge: NSInteger = 0
    • ViewController.swift

      1
      2
      3
      4
      var kvcObject = KvcClass()

      var subKVCObject = SubKvcClass()
      kvcObject.subKVC = subKVCObject

3.1 通过 键值编码 给对象的属性动态赋值

  • 必须得有标准的 getter 和 setter 方法,或者用 @property 声明。

  • 调用 setValue: forKey: 方法以字符串的方式向对象发送消息,设置实例变量的值。第一个参数是要设置的值,第二个参数是实例变量的名称。

  • 调用 valueForKey: 来获取实例变量的值。

  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    // 动态设置属性的值
    [kvcObject setValue:@"xiao bai" forKey:@"name"];
    [kvcObject setValue:@"8" forKey:@"age"];

    // 获取实例变量的值
    NSString *name = [kvcObject valueForKey:@"name"];
    NSInteger age = [[kvcObject valueForKey:@"age"] integerValue];
  • Swift

    1
    2
    3
    4
    5
    6
    7
    // 动态设置属性的值
    kvcObject.setValue("xiao bai", forKey: "name")
    kvcObject.setValue("8", forKey: "age")

    // 获取实例变量的值
    let name1 = kvcObject.valueForKey("name") as! String
    let age1 = kvcObject.valueForKey("age") as! NSInteger

3.2 通过 键路径 给实例变量是其他类的对象赋值

  • 如果实例变量中有其他类的对象,那么可以使用 setValue: forKeyPath: 给其他类的对象的属性变量赋值。

  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    // 通过键路径给 KVCClass 中的对象的属性赋值
    [kvcObject setValue:@"sub xiao bai" forKeyPath:@"subKVC.subName"];
    [kvcObject setValue:@"5" forKeyPath:@"subKVC.subAge"];

    // 获取 KVCClass 中的对象的属性值
    NSString *subName = [kvcObject valueForKeyPath:@"subKVC.subName"];
    NSInteger subAage = [[kvcObject valueForKeyPath:@"subKVC.subAge"] integerValue];
  • Swift

    1
    2
    3
    4
    5
    6
    7
    // 通过键路径给 KvcClass 中的对象的属性赋值
    kvcObject.setValue("sub xiao bai", forKeyPath: "subKVC.subName")
    kvcObject.setValue("5", forKeyPath: "subKVC.subAge")

    // 获取 KvcClass 中的对象的属性值
    let subName1 = kvcObject.valueForKeyPath("subKVC.subName") as! String
    let subAage1 = kvcObject.valueForKeyPath("subKVC.subAge") as! NSInteger

3.3 通过 字典 给对象的属性动态赋值

  • 字典中的 key 值需与要赋值的对象的属性变量名相同。并且都为字符串类型。

    1
    2
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    NSDictionary *kvcDic = @{@"name":@"xiaobai", @"age":@"6"};

    // 以字典的 key 和 value 值分别作为 kvc 的 key 和 value 存储
    [kvcObject setValuesForKeysWithDictionary:kvcDic];

    // 取值,获取指定 keys 值对应的 values
    NSDictionary *dicValue = [kvcObject dictionaryWithValuesForKeys:@[@"name", @"age"]];
  • Swift

    1
    2
    3
    4
    5
    6
    7
    let kvcDic = ["name": "xiaobai", "age": "6"]

    // 以字典的 key 和 value 值分别作为 kvc 的 key 和 value 存储
    kvcObject.setValuesForKeysWithDictionary(kvcDic)

    // 取值,获取指定 keys 值对应的 values
    let dicValue = kvcObject.dictionaryWithValuesForKeys(["name", "age"])

4、KVC 异常处理

4.1 数据冗余处理

  • 在键值编码的类中使用以下两个方法处理 key 值不存在的异常。如果不做处理,编译时系统会报错。

    1
    2
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key;
    - (id)valueForUndefinedKey:(NSString *)key;
  • Objective-C

    • KvcClass.m

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 设置不存在 key 的值
      - (void)setValue:(id)value forUndefinedKey:(NSString *)key {

      NSLog(@"key 值 %@ 不存在,无法设置值 !", key);
      }

      // 获取不存在的 key 的值
      - (id)valueForUndefinedKey:(NSString *)key {

      NSLog(@"key 值 %@ 不存在,无法获取值 !", key);

      return nil;
      }
    • ViewController.m

      1
      2
      3
      4
      5
      6
      7
      8
      // 对象 kvcObject 没有 score 属性,出现数据异常
      [kvcObject setValue:@"99" forKey:@"score"];
      [kvcObject valueForKey:@"score"];

      NSDictionary *kvcDic1 = @{@"name":@"xiaobai", @"age":@"6", @"score":@"100"};

      // 对象 kvcObject 没有 score 属性,出现数据异常
      [kvcObject setValuesForKeysWithDictionary:kvcDic1];
  • Swift

    • KvcClass.swift

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 设置不存在 key 的值
      override func setValue(value: AnyObject?, forUndefinedKey key: String) {

      print("key 值 \(key) 不存在,无法设置值 !")
      }

      // 获取不存在的 key 的值
      override func valueForUndefinedKey(key: String) -> AnyObject? {

      print("key 值 \(key) 不存在,无法获取值 !")

      return nil
      }
    • ViewController.swift

      1
      2
      3
      4
      5
      6
      7
      // 对象 kvcObject 没有 score 属性,出现数据异常
      kvcObject.setValue("99", forKey: "score")
      kvcObject .valueForKey("score")

      // 对象 kvcObject 没有 score 属性,出现数据异常
      let kvcDic1 = ["name":"xiaobai", "age":"6", "score":"99"]
      kvcObject.setValuesForKeysWithDictionary(kvcDic1)

4.2 key 为系统关键字处理

  • 在需要处理的数据源中有系统关键字时,在键值编码处理的类中使用以下两个方法处理 key 值为系统关键字的情况。

    1
    2
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key;
    - (id)valueForUndefinedKey:(NSString *)key;
  • id 是服务端最喜欢用的属性,id 在 iOS 中是关键字,但在模型中可以正常使用的。

  • Objective-C

    • KvcClass.m

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 设置不存在 key 的值
      - (void)setValue:(id)value forUndefinedKey:(NSString *)key {

      // id 为系统关键字,用 ID 代替与系统冲突的 id
      if ([key isEqualToString:@"id"]) {

      // 设置自定义的 key 的值
      self.ID = [(NSString *)value integerValue];
      }
      }

      // 获取不存在的 key 的值
      - (id)valueForUndefinedKey:(NSString *)key {

      // id 为系统关键字,用 ID 代替与系统冲突的 id
      if ([key isEqualToString:@"id"]) {

      // 获取自定义的 key 的值
      return [NSString stringWithFormat:@"%li", self.ID];
      }

      return nil;
      }
    • ViewController.m

      1
      2
      3
      4
      5
      6
      7
      // id 为系统关键字
      [kvcObject setValue:@"3" forKey:@"id"];
      NSInteger ID1 = [[kvcObject valueForKey:@"id"] integerValue];

      NSDictionary *kvcDic2 = @{@"name":@"xiaobai", @"age":@"6", @"id":@"5"};
      [kvcObject setValuesForKeysWithDictionary:kvcDic2];
      NSInteger ID2 = kvcObject.ID;
  • Swift

    • KvcClass.swift

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 设置不存在 key 的值
      override func setValue(value: AnyObject?, forUndefinedKey key: String) {

      // id 为系统关键字,用 ID 代替与系统冲突的 id
      if key == "id" {

      // 设置自定义的 key 的值
      self.ID = (value as! NSString).integerValue
      }
      }

      // 获取不存在的 key 的值
      override func valueForUndefinedKey(key: String) -> AnyObject? {

      // id 为系统关键字,用 ID 代替与系统冲突的 id
      if key == "id" {

      // 获取自定义的 key 的值
      return NSString(format: "%li", self.ID)
      }

      return nil
      }
    • ViewController.swift

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // id 为系统关键字
      kvcObject.setValue("3", forKey: "id")

      let ID1 = (kvcObject.valueForKey("id") as! NSString).integerValue

      // id 为系统关键字
      let kvcDic2 = ["name":"xiaobai", "age":"6", "id":"5"]

      kvcObject.setValuesForKeysWithDictionary(kvcDic2)
      let ID2 = kvcObject.ID

5、KVC 消息传递

  • valueForKey: 的使用并不仅仅用来取值那么简单,还有很多特殊的用法,集合类也覆盖了这个方法,通过调用 valueForKey: 给容器中每一个对象发送操作消息,并且结果会被保存在一个新的容器中返回,这样我们能很方便地利用一个容器对象创建另一个容器对象。另外,valueForKeyPath: 还能实现多个消息的传递。

  • Objective-C

    1
    2
    3
    4
    NSArray *array = @[@"10.11", @"20.22"];

    // 结果为 (10, 20)
    NSArray *resultArray = [array valueForKeyPath:@"doubleValue.intValue"];
  • Swift

    1
    2
    3
    4
    let array: NSArray = ["10.11", "20.22"]

    // 结果为 (10, 20)
    let resultArray: AnyObject? = array.valueForKeyPath("doubleValue.intValue")

6、KVC 字典转模型 数据冗余处理

  • 字典中元素与模型中的属性数量不想等的情况处理。处理 key 值不存在的异常。如果不做处理,编译时系统会报错。

6.1 系统方式

  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    + (instancetype)newsModelWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];

    [obj setValuesForKeysWithDictionary:dict];

    return obj;
    }

    /// 重写系统方法

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {

    }

6.2 列举属性数组方式

  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    + (instancetype)newsModelWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];

    [obj setValueWithDict:dict];

    return obj;
    }

    - (instancetype)setValueWithDict:(NSDictionary *)dict {

    // 列出所有使用的属性
    NSArray *properties = @[@"title", @"digest", @"imgsrc", @"replyCount"];

    for (NSString *key in properties) {

    // 判断字典中是否包含 key
    if (dict[key] != nil) {

    // 每一个属性使用 kvc 设置数值
    [self setValue:dict[key] forKey:key];
    }
    }
    return self;
    }

6.3 运行时动态获取对象属性方式

  • Objective-C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    + (instancetype)newsModelWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];

    [obj setValueWithDict:dict];

    return obj;
    }

    /// 使用运行时动态获取对象属性

    - (instancetype)setValueWithDict:(NSDictionary *)dict {

    unsigned int count = 0;

    // 拷贝对象属性数组(数组名就是指向数组第一个元素的地址)
    objc_property_t *properties = class_copyPropertyList(self.class, &count);

    // 遍历数组
    for (unsigned int i = 0; i < count; ++i) {

    // 从数组中获取属性
    objc_property_t pty = properties[i];

    // 获取属性名称
    const char *cname = property_getName(pty);

    NSString *key = [NSString stringWithUTF8String:cname];

    if (dict[key] != nil) {
    [self setValue:dict[key] forKey:key];
    }
    }

    // 释放数组
    free(properties);

    return self;
    }
文章目录
  1. 1. 1、KVC
  2. 2. 2、数据模型
  3. 3. 3、KVC 赋值与取值
    1. 3.1. 3.1 通过 键值编码 给对象的属性动态赋值
    2. 3.2. 3.2 通过 键路径 给实例变量是其他类的对象赋值
    3. 3.3. 3.3 通过 字典 给对象的属性动态赋值
  4. 4. 4、KVC 异常处理
    1. 4.1. 4.1 数据冗余处理
    2. 4.2. 4.2 key 为系统关键字处理
  5. 5. 5、KVC 消息传递
  6. 6. 6、KVC 字典转模型 数据冗余处理
    1. 6.1. 6.1 系统方式
    2. 6.2. 6.2 列举属性数组方式
    3. 6.3. 6.3 运行时动态获取对象属性方式
隐藏目录