iOS KVC詳解

KVC定義

KVC(Key-value coding)鍵值編碼,就是指iOS開發(fā)中,可以允許開發(fā)中通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調(diào)用明確的存取方法。這樣就可以再運行時動態(tài)的訪問和修改對象的屬性。而不是在編譯時確定,這也是iOS開發(fā)中的黑魔法之一。很多高級的的iOS開發(fā)技巧都是基于KVC實現(xiàn)的。

KVC的一些妙用

  • 利用KVC可以輕松的取得一個數(shù)組的最大值、最小值、平均數(shù)、求和。
  • 利用KVC可以刪除數(shù)組中的重復數(shù)據(jù)。

KVC常用方法

KVC的定義都是對NSObject的擴展來實現(xiàn)的,Objective-C中有個顯式的NSKeyValueCoding類別名,所以對于所有繼承了NSObject的類型,都能使用KVC。一些純Swift類和結(jié)構(gòu)體是不支持KVC的,因為沒有繼承NSObject。
KVC最為重要的四個方法:

- (nullable id)valueForKey:(NSString *)key;                          //直接通過Key來取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通過Key來設(shè)值

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過KeyPath來設(shè)值
  • valueForKey只能取到對象的一級屬性
  • valueForKeyPath,多了個path,所以可以取到路徑下的任一屬性,該路徑以點分割。
    NSKeyValueCoding類別中其他的一些方法:
+ (BOOL)accessInstanceVariablesDirectly;
//默認返回YES,表示如果沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就不這樣搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性?驗證的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性,則會調(diào)用這個方法,默認是拋出異常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法一樣,但這個方法是設(shè)值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時面給Value傳nil,則會調(diào)用這個方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。

同時蘋果對一些容器類比如NSArray或者NSSet等,KVC有著特殊的實現(xiàn)。

  • 有序集合對應方法如下:
-countOf<Key>//必須實現(xiàn),對應于NSArray的基本方法count:2  -objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個必須實現(xiàn)一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之
  • 無序集合對應方法如下:
-countOf<Key>//必須實現(xiàn),對應于NSArray的基本方法count:

-objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個必須實現(xiàn)一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://這兩個都是可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之

KVC相關(guān)概念及應用

接下來將通過一下幾個方面講解KVC相關(guān)技術(shù)概念及使用

  • KVC設(shè)值
  • KVC取值
  • KVC使用keyPath
  • KVC處理異常
  • KVC處理數(shù)值和結(jié)構(gòu)體類型屬性
  • KVC鍵值驗證(Key-Value Validation)
  • KVC處理集合
  • KVC處理字典

1. KVC設(shè)值

KVC要設(shè)值,就要找到對象中對應的key。當代用setValue:屬性值forKey:@“name”的代碼時,底層的執(zhí)行機制如下:

  • 程序優(yōu)先調(diào)用set<key>:屬性值方法,代碼通過setter方法完成設(shè)值。注意,這里的<key>是指成員變量名。
  • 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執(zhí)行setValue:forUndefinedKey:方法,不過一般開發(fā)者不會這么做。所以KVC機制會搜索該類里面有沒有名為<key>的成員變量,無論該變量是在類接口處定義,還是在類實現(xiàn)處定義,也無論用了什么樣的訪問修飾符,只在存在以<key>命名的變量,KVC都可以對該成員變量賦值。
  • 如果該類既沒有set<key>:方法,也沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。
  • 和上面一樣,如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續(xù)搜索<key>和is<Key>的成員變量。再給它們賦值。
  • 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。

簡單來說就是:如果沒有找到set<key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員并進行賦值。
如果開發(fā)者想讓這個類里禁用KVC,那么重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時,會直接用setValue:forUndefinedKey:方法。
下面看例子:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_name;
}

@end

@implementation Test

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //通過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] obj的名字是xiaoming

可以看到通過- (void)setValue:(nullable id)value forKey:(NSString *)key;- (nullable id)valueForKey:(NSString *)key;成功設(shè)置和取出obj對象的name值。
再看一下設(shè)置accessInstanceVariablesDirectly為NO的效果:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_name;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //通過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] 出現(xiàn)異常,該key不存在name
2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] 出現(xiàn)異常,該key不存在name
2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj的名字是(null)

可以看到accessInstanceVariablesDirectly為NO的時候KVC只會查詢setter和getter這一層,下面尋找key的相關(guān)變量執(zhí)行就會停止,直接報錯。
設(shè)置accessInstanceVariablesDirectly為YES,再修改_name為_isName,看看執(zhí)行是否成功。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_isName;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //通過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] obj的名字是xiaoming

從打印可以看到設(shè)置accessInstanceVariablesDirectly為YES,KVC會繼續(xù)按照順序查找,并成功設(shè)值和取值了。

2. KVC取值

當調(diào)用valueForKey:@”name“的代碼時,KVC對key的搜索方式不同于setValue:屬性值 forKey:@”name“,其搜索方式如下:

  • 首先按get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話會直接調(diào)用。如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
  • 如果上面的getter沒有找到,KVC則會查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調(diào)用這個代理集合的方法,或者說給這個代理集合發(fā)送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個方法組合的形式調(diào)用。還有一個可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
  • 如果上面的方法沒有找到,那么會同時查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發(fā)NSSet的消息,就會以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用。
  • 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),那么和先前的設(shè)值一樣,會按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會直接調(diào)用valueForUndefinedKey:方法,默認是拋出異常。

給Test類添加getAge方法,例如如下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)getAge {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:00:04.207857+0800 KVCKVO[35324:6188613] obj的年齡是10

可以看到[obj valueForKey:@"age"],找到了getAge方法,并且取到了值。
下面把getAge改成age,例子如下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)age {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:02:27.270954+0800 KVCKVO[35337:6195086] obj的年齡是10

可以看到[obj valueForKey:@"age"],找到了age方法,并且取到了值。

下面把getAge改成isAge,例子如下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)isAge {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //通過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:03:56.234338+0800 KVCKVO[35345:6201242] obj的年齡是10

可以看到[obj valueForKey:@"age"],找到了isAge方法,并且取到了值。
上面的代碼說明了說明了KVC在調(diào)用valueforKey:@"age"時搜索key的機制。

3. KVC使用keyPath

在開發(fā)過程中,一個類的成員變量有可能是自定義類或其他的復雜數(shù)據(jù)類型,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性,
但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過KeyPath來設(shè)值

用代碼實現(xiàn)如下:

#import <Foundation/Foundation.h>

@interface Test1: NSObject {
    NSString *_name;
}
@end

@implementation Test1
@end

@interface Test: NSObject {
    Test1 *_test1;
}

@end

@implementation Test
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //Test1生成對象
        Test1 *test1 = [[Test1 alloc] init];
        //通過KVC設(shè)值test的"test1"
        [test setValue:test1 forKey:@"test1"];
        //通過KVC設(shè)值test的"test1的name"
        [test setValue:@"xiaoming" forKeyPath:@"test1.name"];
        //通過KVC取值age打印
        NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:19:02.613394+0800 KVCKVO[35436:6239788] test的"test1的name"是xiaoming

從打印結(jié)果來看我們成功的通過keyPath設(shè)置了test1的值。
KVC對于keyPath是搜索機制第一步就是分離key,用小數(shù)點.來分割key,然后再像普通key一樣按照先前介紹的順序搜索下去。

4. KVC處理異常

KVC中最常見的異常就是不小心使用了錯誤的key,或者在設(shè)值中不小心傳遞了nil的值,KVC中有專門的方法來處理這些異常。

KVC處理nil異常

通常情況下,KVC不允許你要在調(diào)用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。很簡單,因為值類型是不能為nil的。如果你不小心傳了,KVC會調(diào)用setNilValueForKey:方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。
代碼實現(xiàn)如下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger age;
}

@end

@implementation Test

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能將%@設(shè)成nil", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //通過KVC設(shè)值test的age
        [test setValue:nil forKey:@"age"];
        //通過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:24:30.302134+0800 KVCKVO[35470:6258307] 不能將age設(shè)成nil
2018-05-05 16:24:30.302738+0800 KVCKVO[35470:6258307] test的年齡是0

KVC處理UndefinedKey異常

通常情況下,KVC不允許你在調(diào)用setValue:屬性值 forKey:(或者keyPath)時對不存在的key進行操作。
不然,會報錯forUndefinedKey發(fā)生崩潰,重寫forUndefinedKey方法避免崩潰。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //通過KVC設(shè)值test的age
        [test setValue:@10 forKey:@"age"];
        //通過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] 出現(xiàn)異常,該key不存在age
2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] 出現(xiàn)異常,該key不存在age
2018-05-05 16:30:18.565216+0800 KVCKVO[35487:6277523] test的年齡是(null)

5. KVC處理數(shù)值和結(jié)構(gòu)體類型屬性

不是每一個方法都返回對象,但是valueForKey:總是返回一個id對象,如果原本的變量類型是值類型或者結(jié)構(gòu)體,返回值會封裝成NSNumber或者NSValue對象。
這兩個類會處理從數(shù)字、布爾值到指針和結(jié)構(gòu)體任何類型。然后開發(fā)者需要手動轉(zhuǎn)換成原來的類型。
盡管valueForKey:會自動將值類型封裝成對象,但是setValue:forKey:卻不行。你必須手動將值類型轉(zhuǎn)換成NSNumber或者NSValue類型,才能傳遞過去。
因為傳遞進去和取出來的都是id類型,所以需要開發(fā)者自己擔保類型的正確性,運行時Objective-C在發(fā)送消息的會檢查類型,如果錯誤會直接拋出異常。

舉個例子,Person類有個NSInteger類型的age屬性,如下:

//  Person.m
#import "Person.h"
 
@interface Person ()
 
@property (nonatomic,assign) NSInteger age;
 
@end
 
 
@implementation Person
 
@end

修改值
我們通過KVC技術(shù)使用如下方式設(shè)置age屬性的值:

[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];

我們賦給age的是一個NSNumber對象,KVC會自動的將NSNumber對象轉(zhuǎn)換成NSInteger對象,然后再調(diào)用相應的訪問器方法設(shè)置age的值。
獲取值
同樣,以如下方式獲取age屬性值:

[person valueForKey:@"age"];

這時,會以NSNumber的形式返回age的值。

//  ViewController.m
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc]init];
    [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
    NSLog(@"age=%@",[person valueForKey:@"age"]);
    
}


@end

打印結(jié)果:
2017-01-16 16:31:55.709 Test[28586:2294177] age=5

需要注意的是我們不能直接將一個數(shù)值通過KVC賦值的,我們需要把數(shù)據(jù)轉(zhuǎn)為NSNumber和NSValue類型傳入,那到底哪些類型數(shù)據(jù)要用NSNumber封裝哪些類型數(shù)據(jù)要用NSValue封裝呢?看下面這些方法的參數(shù)類型就知道了:
可以使用NSNumber的數(shù)據(jù)類型有:

+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);

即一些常見的數(shù)值型數(shù)據(jù)。

可以使用NSValue的數(shù)據(jù)類型有:

+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

NSValue主要用于處理結(jié)構(gòu)體型的數(shù)據(jù),它本身提供了如上集中結(jié)構(gòu)的支持。任何結(jié)構(gòu)體都是可以轉(zhuǎn)化成NSValue對象的,包括其它自定義的結(jié)構(gòu)體。

6. KVC鍵值驗證(Key-Value Validation)

KVC提供了驗證Key對應的Value是否可用的方法:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

該方法默認的實現(xiàn)是調(diào)用一個如下格式的方法:

- (BOOL)validate<Key>:error:

例如:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger _age;
}

@end

@implementation Test

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //通過KVC設(shè)值test的age
        NSNumber *age = @10;
        NSError* error;
        NSString *key = @"age";
        BOOL isValid = [test validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"鍵值匹配");
            [test setValue:age forKey:key];
        }
        else {
            NSLog(@"鍵值不匹配");
        }
        //通過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] 鍵值不匹配
2018-05-05 16:59:06.112215+0800 KVCKVO[35777:6329982] test的年齡是0

這樣就給了我們一次糾錯的機會。需要指出的是,KVC是不會自動調(diào)用鍵值驗證方法的,就是說我們?nèi)绻胍I值驗證則需要手動驗證。但是有些技術(shù),比如CoreData會自動調(diào)用。

7. KVC處理集合

KVC同時還提供了很復雜的函數(shù),主要有下面這些:

簡單集合運算符

簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,都表示什么直接看下面例子就明白了, 目前還不支持自定義。

#import <Foundation/Foundation.h>

@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 10;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 40;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f",sum.floatValue);
        NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f",avg.floatValue);
        NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f",count.floatValue);
        NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f",min.floatValue);
        NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f",max.floatValue);
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 17:04:50.674243+0800 KVCKVO[35785:6351239] sum:100.000000
2018-05-05 17:04:50.675007+0800 KVCKVO[35785:6351239] avg:25.000000
2018-05-05 17:04:50.675081+0800 KVCKVO[35785:6351239] count:4.000000
2018-05-05 17:04:50.675146+0800 KVCKVO[35785:6351239] min:10.000000
2018-05-05 17:04:50.675204+0800 KVCKVO[35785:6351239] max:40.000000

對象運算符

比集合運算符稍微復雜,能以數(shù)組的方式返回指定的內(nèi)容,一共有兩種:

  • @distinctUnionOfObjects
  • @unionOfObjects
    它們的返回值都是NSArray,區(qū)別是前者返回的元素都是唯一的,是去重以后的結(jié)果;后者返回的元素是全集。
    例如:
#import <Foundation/Foundation.h>

@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 40;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 10;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        
        NSLog(@"distinctUnionOfObjects");
        NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        for (NSNumber *price in arrDistinct) {
            NSLog(@"%f",price.floatValue);
        }
        NSLog(@"unionOfObjects");
        NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        for (NSNumber *price in arrUnion) {
            NSLog(@"%f",price.floatValue);
        }
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 17:06:21.832401+0800 KVCKVO[35798:6358293] distinctUnionOfObjects
2018-05-05 17:06:21.833079+0800 KVCKVO[35798:6358293] 10.000000
2018-05-05 17:06:21.833112+0800 KVCKVO[35798:6358293] 20.000000
2018-05-05 17:06:21.833130+0800 KVCKVO[35798:6358293] 30.000000
2018-05-05 17:06:21.833146+0800 KVCKVO[35798:6358293] 40.000000
2018-05-05 17:06:21.833165+0800 KVCKVO[35798:6358293] unionOfObjects
2018-05-05 17:06:21.833297+0800 KVCKVO[35798:6358293] 40.000000
2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000
2018-05-05 17:06:21.833371+0800 KVCKVO[35798:6358293] 30.000000
2018-05-05 17:06:21.833388+0800 KVCKVO[35798:6358293] 10.000000

8. KVC處理字典

當對NSDictionary對象使用KVC時,valueForKey:的表現(xiàn)行為和objectForKey:一樣。所以使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。
KVC里面還有兩個關(guān)于NSDictionary的方法:

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

dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對應的屬性,再組成一個字典。
setValuesForKeysWithDictionary是用來修改Model中對應key的屬性。下面直接用代碼會更直觀一點:

#import <Foundation/Foundation.h>

@interface Address : NSObject

@end

@interface Address()

@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;

@end

@implementation Address

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //模型轉(zhuǎn)字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key所有的屬性全部取出來
        NSLog(@"%@",dict);
        
        //字典轉(zhuǎn)模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];            //用key Value來修改Model的屬性
        NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);
        
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province:california city:Los angle

打印出來的結(jié)果完全符合預期。

KVC使用

KVC在iOS開發(fā)中是絕不可少的利器,這種基于運行時的編程方式極大地提高了靈活性,簡化了代碼,甚至實現(xiàn)很多難以想像的功能,KVC也是許多iOS開發(fā)黑魔法的基礎(chǔ)。
下面列舉iOS開發(fā)中KVC的使用場景.

1. 動態(tài)地取值和設(shè)值

利用KVC動態(tài)的取值和設(shè)值是最基本的用途了。

2. 用KVC來訪問和修改私有變量

對于類里的私有屬性,Objective-C是無法直接訪問的,但是KVC是可以的。(但目前蘋果對私有屬性的訪問很敏感,謹慎使用)

3. Model和字典轉(zhuǎn)換

這是KVC強大作用的又一次體現(xiàn),KVC和Objc的runtime組合可以很容易的實現(xiàn)Model和字典的轉(zhuǎn)換。

4. 修改一些控件的內(nèi)部屬性

這也是iOS開發(fā)中必不可少的小技巧。眾所周知很多UI控件都由很多內(nèi)部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API,這樣我們就無法正常地訪問和修改這些控件的樣式。
而KVC在大多數(shù)情況可下可以解決這個問題。最常用的就是個性化UITextField中的placeHolderText了。

5. 操作集合

Apple對KVC的valueForKey:方法作了一些特殊的實現(xiàn),比如說NSArray和NSSet這樣的容器類就實現(xiàn)了這些方法。所以可以用KVC很方便地操作集合。

6. 用KVC實現(xiàn)高階消息傳遞

當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是容器本身進行操作。結(jié)果會被添加進返回的容器中,這樣,開發(fā)者可以很方便的操作集合來返回另一個集合。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSArray* arrStr = @[@"english",@"franch",@"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString* str  in arrCapStr) {
            NSLog(@"%@",str);
        }
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue);
        }
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] English
2018-05-05 17:16:21.976296+0800 KVCKVO[35824:6395514] Franch
2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese
2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7
2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 6
2018-05-05 17:16:21.976550+0800 KVCKVO[35824:6395514] 7

方法capitalizedString被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執(zhí)行capitalizedString并返回一個包含結(jié)果的新的NSArray。
從打印結(jié)果可以看出,所有String都成功以轉(zhuǎn)成了大寫。
同樣如果要執(zhí)行多個方法也可以用valueForKeyPath:方法。它先會對每一個成員調(diào)用 capitalizedString方法,然后再調(diào)用length,因為lenth方法返回是一個數(shù)字,所以返回結(jié)果以NSNumber的形式保存在新數(shù)組里。

文章摘自iOS KVC和KVO詳解,在此基礎(chǔ)上加以加工,此文僅作為自己學習記錄使用,如有問題,歡迎指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容