iOS - KVC 鍵值編碼

weixin_34041003發表於2016-08-14

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 如果寫錯了,編譯器不會有任何警告和報錯,造成設錯資料或者取錯資料。如:

            dict[@"name"] = @"Jack";
            NSString *name = dict[@"name"];
    • 使用模型的好處:
      • 所謂模型,其實就是資料模型,專門用來存放資料的物件,用它來表示資料會更加專業。模型設定資料和取出資料都是通過它的屬性,屬性名如果寫錯了,編譯器會馬上報錯,因此,保證了資料的正確性
        使用模型訪問屬性時,編譯器會提供一系列的提示,提高編碼效率。

            app.name = @"Jack";
            NSString *name = app.name;
  • 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)字典轉模型的過程

    KVC1

3、KVC 賦值與取值

  • KVC 是一種操作全域性變數的方法,無論是公有的,私有的,還是受保護的全都可以操作。

    • 1、找物件的 setter 方法,找到就執行;
    • 2、找不到 setter 方法就找 _name 變數,找到就賦值;
    • 3、如果找不到 _name 變數,就找 name;
    • 4、如果 name 也找不到就會讓物件呼叫 -(void)setValue:forUnderfinedKey; 方法處理異常。
  • Objective-C

    • KvcClass.h

          @property(nonatomic, assign) NSInteger ID;
      
          @property(nonatomic, copy) NSString *name;
          @property(nonatomic, assign) NSInteger age;
      
          @property(nonatomic, retain) SubKvcClass *subKVC;
    • SubKvcClass.h

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

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

    • KvcClass.swift

          var ID:NSInteger!
      
          var name:String!
          var age:NSInteger = 0
      
          var subKVC:SubKvcClass!
    • SubKvcClass.swift

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

          var kvcObject = KvcClass()
      
          var subKVCObject = SubKvcClass()
          kvcObject.subKVC = subKVCObject

3.1 通過 鍵值編碼 給物件的屬性動態賦值

  • 必須得有標準的 getter 和 setter 方法,或者用 @property 宣告。

  • 呼叫 setValue: forKey: 方法以字串的方式向物件傳送訊息,設定例項變數的值。第一個引數是要設定的值,第二個引數是例項變數的名稱。
  • 呼叫 valueForKey: 來獲取例項變數的值。

  • Objective-C

        // 動態設定屬性的值
        [kvcObject setValue:@"xiao bai" forKey:@"name"];
        [kvcObject setValue:@"8" forKey:@"age"];
    
        // 獲取例項變數的值
        NSString *name = [kvcObject valueForKey:@"name"];
        NSInteger age = [[kvcObject valueForKey:@"age"] integerValue];
  • Swift

        // 動態設定屬性的值
        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

        // 通過鍵路徑給 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

        // 通過鍵路徑給 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 值需與要賦值的物件的屬性變數名相同。並且都為字串型別。

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

        NSDictionary *kvcDic = @{@"name":@"xiaobai", @"age":@"6"};
    
        // 以字典的 key 和 value 值分別作為 kvc 的 key 和 value 儲存
        [kvcObject setValuesForKeysWithDictionary:kvcDic];
    
        // 取值,獲取指定 keys 值對應的 values
        NSDictionary *dicValue = [kvcObject dictionaryWithValuesForKeys:@[@"name", @"age"]];
  • Swift

        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 值不存在的異常。如果不做處理,編譯時系統會報錯。

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

    • KvcClass.m

          // 設定不存在 key 的值
          - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
      
              NSLog(@"key 值 %@ 不存在,無法設定值 !", key);
          }
      
          // 獲取不存在的 key 的值
          - (id)valueForUndefinedKey:(NSString *)key {
      
              NSLog(@"key 值 %@ 不存在,無法獲取值 !", key);
      
              return nil;
          }
    • ViewController.m

          // 物件 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

          // 設定不存在 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

          // 物件 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 值為系統關鍵字的情況。

        - (void)setValue:(id)value forUndefinedKey:(NSString *)key;
        - (id)valueForUndefinedKey:(NSString *)key;
  • id 是服務端最喜歡用的屬性,id 在 iOS 中是關鍵字,但在模型中可以正常使用的。

  • Objective-C

    • KvcClass.m

          // 設定不存在 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

          // 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

          // 設定不存在 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

          // 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

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

        let array:NSArray = ["10.11", "20.22"]
    
        // 結果為 (10, 20)
        let resultArray:AnyObject? = array.valueForKeyPath("doubleValue.intValue")

6、KVC 字典轉模型 資料冗餘處理

  • 字典中元素與模型中的屬性數量不想等的情況處理。處理 key 值不存在的異常。如果不做處理,編譯時系統會報錯。

6.1 系統方式

  • Objective-C

        + (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

        + (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

        + (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;
            }

相關文章