複習一下KVC

一. 前言

KVC(Key Value Coding)是Cocoa框架爲開發者提供的很是強大的工具,簡單解釋爲:鍵值編碼。它依賴於Runtime,在OC的動態性方面發揮了重要做用。
它主要的功能在於直接經過變量名稱字符串來訪問成員變量,不論是私有的仍是共有的,這也是爲何對於OC來講沒有真正的私有變量,由於它們均可以使用KVC訪問。html

二. 使用場景

下面是KVC的一些實用場景,讀者可自行編碼嘗試。git

1.訪問私有屬性

例如設置UITextField的placeholder顏色,常規的方法是:github

// 方式一:常規設置
 _nameTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"請輸入名字" attributes:@{NSForegroundColorAttributeName : [UIColor redColor]}];

經過KVC的方式設置:數組

// 方式二:使用KVC獲取私有屬性
_nameTextField.placeholder = @"請輸入名字";
// UILabel *placeHolderLabel = [_nameTextField valueForKey:@"placeholderLabel"];
UILabel *placeHolderLabel = [_nameTextField valueForKey:@"_placeholderLabel"];
placeHolderLabel.textColor = UIColor.blackColor;
[self.view addSubview:_nameTextField];

這裏經過valueForKey的方式獲取了UITextField的placeholderLabel,而後對該對象的顏色進行設置。若是獲取到的placeholderLabel爲nil,有幾種可能:安全

  1. key輸入有誤(從新輸入)
  2. 系統實現發生改變
  3. 蘋果使用的Lazy Load,致使在使用valueForKey獲取的時候尚未初始化(所以這裏先賦值placeholder,而後再獲取placeholderLabel)

仍是建議使用使用方式一對屬性進行設置。站在蘋果的角度,它之因此把某些屬性設置爲私有,就是不想讓開發者進行直接修改,後續一旦蘋果對系統實現有所更改,那就會致使使用KVC獲取的內容失效。
另外,在使用setValue:forKey:的時候必定要類型統一,好比你經過key獲取到的是一個Label,卻將string設置爲了value,將會crash。
上面在使用valueForKey:方法的時候參數能夠帶下劃線( _ placeholderLabel ),也能夠不帶下劃線,它的主要區別就是若是使用了帶下劃線的key,就算類中手動實現了getter方法,也不會執行類中實現的getter方法。若是使用了不帶下劃線的,將會執行類中getter方法。setter也是如此。app

2.對象關係映射

在沒有比較成熟的第三方Model解析(如Mantle)前,ORM(Object Relational Mapping)可使用KVC進行處理:框架

- (instancetype)init {
    return [self initWithJSONDictionary:nil];
}
- (instancetype)initWithJSONDictionary:(NSDictionary *)anDictionary {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:anDictionary];
    }
    return self;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%@",key);
}
//
- (void)objectRelationalMapping {
    NSDictionary *personInfoDictionary = @{
                                           @"name" : @"zhangsan",
                                           @"age" : @"30",
                                           @"school" : @"Hist"
                                           };
    Person *p1 = [[Person alloc] initWithJSONDictionary:personInfoDictionary];
    NSLog(@"%@",p1);
}
@end

這裏主要使用了setValuesForKeysWithDictionary:方法。須要注意的是,須要實現setValue:forUndefinedKey:方法,由於當字典中包含的key在Person屬性中並不必定存在,若是不存在的話,就會調用setValue:forUndefinedKey:。該方法默認拋出NSUndefinedKeyException異常。因此須要對其進行重寫,避免Crash。函數

3. 使用keyPath實現多級訪問

KVC除了setValue:forKey:方法,還有setValue:forKeypath:方法。具體使用以下:工具

_nameTextField.placeholder = @"請輸入名字";
[_nameTextField setValue:UIColor.blackColor forKeyPath:@"placeholderLabel.textColor"];

這裏一步操做,就完成了對placeholder顏色的變動。ui

4. 安全性訪問

如今Person有一個friends方法,屬性聲明以下:

@property (nonatomic, copy) NSMutableArray *friends;

此時能夠經過以下的方式進行設置friends:

NSArray *personsArray = ...;
Person *zhangsan = [[Person alloc] init];
zhangsan.name = @"zhangsan";
[[zhangsan mutableArrayValueForKey:@"friends"] addObjectsFromArray:personsArray];

這樣就能夠順利將personsArray賦值給zhangsan的friends。接下來換個操做:將屬性改成

@property (nonatomic, copy) NSArray *friends;

其餘代碼保持不變。再次執行,依然會給zhangsan.friends賦值。並且沒有任何crash或者異常。因而可知,經過mutableArrayValueForKey這種方式進行處理,能夠對於不可變的集合類型,提供安全的可變訪問,即便是不可變數組,也能夠增長數組元素。

5. 函數操做

使用KVC,能夠很方便地進行一些基本的函數操做,例如:

NSMutableArray *personsArray = [[NSMutableArray alloc] initWithCapacity:5];
for (NSInteger i = 0; i < 5; i ++) {
    NSString *tempName = [NSString stringWithFormat:@"people%ld",(long)i];
    NSDictionary *personInfoDictionary = @{
                                           @"name" : tempName,
                                           @"age" : @(10 + i),
                                           @"school" : @"Hist"
                                           };
    Person *tempPerson = [[Person alloc] initWithJSONDictionary:personInfoDictionary];
    [personsArray addObject:tempPerson];
}
NSNumber *count = [personsArray valueForKeyPath:@"@count"];
NSNumber *sumAge = [personsArray valueForKeyPath:@"@sum.age"];
NSNumber *avgAge = [personsArray valueForKeyPath:@"@avg.age"];
NSNumber *maxAge = [personsArray valueForKeyPath:@"@max.age"];
NSNumber *minAge = [personsArray valueForKeyPath:@"@min.age"];

其中@表示是數組特有的鍵,而不是名爲count的鍵。可使用valueForKeyPath:快速進行計算。還能夠進行更復雜的一些計算:

NSArray *array = @[@"apple", @"banner", @"apple", @"orange"];
NSLog(@"%@", [array valueForKeyPath:@"@distinctUnionOfObjects.self"]); // orange,apple,banner

NSArray *array1 = @[@[@"apple", @"banner"], @[@"apple", @"orange"]];
NSLog(@"%@", [array1 valueForKeyPath:@"@unionOfArrays.self"]); // apple,banner,apple,orange

NSMutableArray *personsArray1 = [[NSMutableArray alloc] initWithCapacity:5];
NSMutableArray *personsArray2 = [[NSMutableArray alloc] initWithCapacity:5];
for (NSInteger i = 0; i < 5; i ++) {
    NSString *tempName = [NSString stringWithFormat:@"people%ld",(long)i];
    NSDictionary *personInfoDictionary = @{
                                           @"name" : tempName,
                                           @"age" : @(10 + i),
                                           @"school" : @"Hist"
                                           };
    Person *tempPerson = [[Person alloc] initWithJSONDictionary:personInfoDictionary];
    i % 2 == 0 ? [personsArray1 addObject:tempPerson] : [personsArray2 addObject:tempPerson];
}
NSArray *personsArray = @[personsArray1, personsArray2];
NSLog(@"%@",[[personsArray valueForKeyPath:@"@unionOfArrays.age"] valueForKeyPath:@"@sum.self"]);  // 60

相似上面的代碼,可使用KVC對複雜的操做進行簡單化,而沒有必要再使用for循環或者其餘遍歷操做。

三. KVC驗證

Person *person = [[Person alloc] init];
[person setValue:[UIColor redColor] forKey:@"name"];

name是string類型,可是傳入一個UIColor類型也沒有異常或者Crash產生,這也是一個潛在的問題,一旦按照string使用name,就會出現問題。所以須要對value類型與key是否匹配進行判斷。KVC提供了以下的方法:

Person *person = [[Person alloc] init];
UIColor *color = [UIColor redColor];
NSError *error = nil;
BOOL isValidate = [person validateValue:&color forKey:@"name" error:&error];
if (isValidate && !error) {
   [person setValue:color forKey:@"name"];
}

發現依然能夠設置成功,validateValue:forKey:error:方法居然返回了YES。根據官方文檔可知,此方法的默認實現將搜索接收方的類,尋找名稱匹配validate :error:模式的驗證方法。若是爲屬性定義了這樣一個方法,那麼validateValue:forKey:error: 的默認實如今須要被驗證的時候調用。在爲這個屬性定義的方法中,你能夠根據須要更改輸入的值,或者設置默認值等。若是沒有實現驗證方法,則默認返回YES。所以,咱們能夠在Person類中實現下面的方法:

- (BOOL)validateName:(id *)ioValue error:(NSError **)error {
    if ([*ioValue isKindOfClass:[NSString class]]) {
        return YES;
    }
    return NO;
}
相關文章
相關標籤/搜索