鍵值編碼API可以直接訪問類的屬性:
@interface Hello : NSObject
@property NSString* greeting;
@end
@implementation Hello
-(id)init{
if ((self = [super init])) {
_greeting = @"Hello";
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Hello *hello = [Hello new];
// 相當於hello.greeting = @"Hi"
[hello setValue:@"Hi" forKey:@"greeting"];
// 相當於NSLog(@"%@",hello.greeting)
NSLog(@"%@",[hello valueForKey:@"greeting"]);
}
return 0;
}
使用鍵值編碼訪問屬性與標准的屬性訪問方法相比,是基於配置的屬性訪問,可以降低耦合性,簡化代碼,易於維護和拓展。
鍵值編碼使用鍵和鍵路徑訪問屬性。鍵是用於標識屬性的字符串。鍵路徑指明了需要遍歷的對象屬性序列。鍵值編碼可以使用點語法的鍵路徑:
@interface Hello : NSObject
@property NSString *greeting;
@end
@implementation Hello
-(id)init{
if ((self = [super init])) {
_greeting = @"Hello";
}
return self;
}
@end
@interface Person : NSObject
@property Hello *hello;
@end
@implementation Person
-(id)init{
if ((self = [super init])) {
_hello = [Hello new];
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person new];
// 相當於person.hello.greeting = @"Hi"
[person setValue:@"Hi" forKeyPath:@"hello.greeting"];
// 相當於NSLog(@"%@",person.hello.greeting)
NSLog(@"%@",[person valueForKeyPath:@"hello.greeting"]);
}
return 0;
}
使用NSObject類遵守的協議MSKeyValueCoding中的valueForUndefinedKey:方法可以處理未定義鍵情況:
@implementation Person
...
-(id)valueForUndefinedKey:(NSString *)key{
// 如果輸入鍵為hi,則返回hello屬性,否則拋出異常
if((nil != key) && ([@"hi" isEqualToString:key])){
return self.hello;
}
[NSException raise:NSUndefinedKeyException format:@"key %@ not defined", key];
return nil;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person new];
// 可以訪問
NSLog(@"%@",[person valueForKeyPath:@"hi.greeting"]);
// 會拋出異常
NSLog(@"%@",[person valueForKeyPath:@"Hi.greeting"]);
}
return 0;
}
鍵值編碼還未檢驗屬性值提供了基礎設施,檢查方法的選擇器為
validate
@implementation Person
...
-(BOOL)validateHello:(id *)value error:(NSError * __autoreleasing *)error{
if (*value == nil) {
if (error != NULL) {
*error = [NSError errorWithDomain:@"Invalid Property Value (nil)" code:1 userInfo:nil];
}
return NO;
}
return YES;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person new];
Hello *hello = [Hello new];
[hello setValue:@"Hi" forKey:@"greeting"];
NSError *error;
BOOL valid = [person validateValue:&hello forKey:@"hello" error:&error];
if (valid) {
[person setValue:hello forKey:@"hello"];
NSLog(@"%@",[person valueForKeyPath:@"hello.greeting"]);
}
}
return 0;
}
鍵值觀察KVO
添加和刪除觀察對象:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person new];
Hello *hello = [Hello new];
// 添加觀察對象
[person addObserver:hello forKeyPath:@"hello" options:NSKeyValueObservingOptionNew context:NULL];
// 刪除觀察對象
[person removeObserver:hello forKeyPath:@"hello"];
}
return 0;
}
鍵值觀察與通知類(NSNotification)的區別:
1.通知類能夠封裝通用消息,使用更廣泛。而鍵值觀察僅支持對象屬性更改通知功能。因此處理純屬性更改情況時,KVO API會更簡單。
2.通知類使用交互的廣播模型,無須接受對象注冊通知功能,即可向一個以上的對象發送消息,即支持同步傳遞通知,也支持異步傳遞通知。而鍵值觀察使用點對點的交互模型。
3.通知機制中,發送者和接受者沒有直接的雙向通信,必須注冊通知才能進行雙向通信。而鍵值觀察中,觀察者也能夠向被觀察者發送消息。
4.通知名稱必須具備唯一性,蘋果官方文檔規定了一系列命名約定將通知名稱沖突的可能性降到最低。而屬性名稱是在類中使用的(命名空間為類),而且被觀察者與觀察者直接綁定,所以不會出現命名沖突。
下面是一個實現KVO的示例:
首先創建一個用於被觀察的Person類:
#import
@interface Person : NSObject
@property(readonly) NSString *fullName; // 只讀屬性,源自firstName和lastName
@property NSString *firstName;
@property NSString *lastName;
-(id)initWithFirstName:(NSString *)fname lastName:(NSString *)lname;
@end
#import "Person.h"
#define CodersErrorDomain @"CodersErrorDomain"
#define kInvalidValueError 1
@implementation Person
-(id)initWithFirstName:(NSString *)fname lastName:(NSString *)lname{
if ((self = [super init])) {
_firstName = fname;
_lastName = lname;
}
return self;
}
-(NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}
// 鍵值檢查方法
-(BOOL)validateLastName:(id *)value error:(NSError * __autoreleasing *)error{
// 檢查lastName是否為空
if (*value == nil) {
if (error != nil) {
NSDictionary *reason = @{NSLocalizedDescriptionKey:@"Last name cannot be nil"};
*error = [NSError errorWithDomain:CodersErrorDomain code:kInvalidValueError userInfo:reason];
}
return NO;
}
// 檢查空值
NSUInteger length = [[(NSString *)*value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length];
if (length == 0) {
if (error != nil) {
NSDictionary *reason = @{NSLocalizedDescriptionKey:@"Last name cannot be nil"};
*error = [NSError errorWithDomain:CodersErrorDomain code:kInvalidValueError userInfo:reason];
}
return NO;
}
return YES;
}
// 注冊依賴鍵,fullName的值依賴於firstName和lastName,這兩個屬性有變動時,應該通知fullName屬性的觀察者
+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"firstName",@"lastName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
接下來創建一個Coder類,作為Person類的觀察者
#import
@class Person;
@interface Coder : NSObject
@property Person *person;
@property NSMutableArray *languages; // 有序集合
@end
#import "Coder.h"
@implementation Coder
// 下面4個方法用於處理有序的一對多關系屬性
-(NSUInteger)countOfLanguages{
return [self.languages count];
}
-(NSString *)objectInLanguagesAtIndex:(NSUInteger)index{
return [self.languages objectAtIndex:index];
}
-(void)insertObject:(NSString *)object inLanguagesAtIndex:(NSUInteger)index{
[self.languages insertObject:object atIndex:index];
}
-(void)removeObjectFromLanguagesAtIndex:(NSUInteger)index{
[self.languages removeObjectAtIndex:index];
}
// 當被觀察屬性的值發生改變時,被觀察對象就會調用觀察對象中的本方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSString *newValue = change[NSKeyValueChangeNewKey];
NSLog(@"Value changed for %@ object, key path: %@, new value :%@", [object className], keyPath, newValue);
}
@end
再創建一個Coders類,測試無序一對多關系屬性:
#import
@class Coder;
@interface Coders : NSObject
@property NSSet *developers; // 無序集合
@end
#import "Coders.h"
@implementation Coders
// 以下3個方法用於處理無序一對多關系屬性
-(NSUInteger)countOfDevelopers{
return [self.developers count];
}
-(NSEnumerator *)enumeratorOfDevelopers{
return [self.developers objectEnumerator];
}
-(Coder *)memberOfDevelopers:(Coder *)object{
return [self.developers member:object];
}
@end
最後在main.m中進行測試:
#import
#import "Person.h"
#import "Coder.h"
#import "Coders.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 創建一個Person對象,並用鍵值編碼獲取該實例的屬性值
Person *curly = [[Person alloc] initWithFirstName:@"Curly" lastName:@"Howard"];
NSLog(@"Person first name:%@",[curly valueForKey:@"firstName"]);
NSLog(@"Person full name:%@",[curly valueForKey:@"fullName"]);
// 創建兩個Coder對象,並用鍵值編碼獲取它們的person和languages屬性
NSArray *langs1 = @[@"Objective-C",@"C"];
Coder *coder1 = [Coder new];
coder1.person = curly;
coder1.languages = [langs1 mutableCopy];
NSLog(@"\nCoder name:%@\n\t languages:%@",[coder1 valueForKeyPath:@"person.fullName"],[coder1 valueForKey:@"languages"]);
Coder *coder2 = [Coder new];
coder2.person = [[Person alloc] initWithFirstName:@"Larry" lastName:@"Fine"];
coder2.languages = [@[@"Objective-C",@"C++"] mutableCopy];
NSLog(@"\nCoder name:%@\n\t languages:%@",[coder2 valueForKeyPath:@"person.fullName"],[coder2 valueForKey:@"languages"]);
// 將Coder對象注冊為Person對象的觀察者,修改Person對象中被觀察的屬性,觀察者將會收到通知
[curly addObserver:coder1 forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:NULL];
curly.lastName = @"Fine";
[curly removeObserver:coder1 forKeyPath:@"fullName"];
// 創建一個Coders對象,並用操作符@count計算Coders對象中集合的對象總數
Coders *bestCoders = [Coders new];
bestCoders.developers = [[NSSet alloc] initWithArray:@[coder1,coder2]];
NSSet* coders = [bestCoders valueForKey:@"developers"];
NSLog(@"Number of coders = %@",[coders valueForKeyPath:@"@count"]);
// 用空字符串測試鍵值編碼檢驗方法
NSError *error;
NSString *emptyName = @"";
BOOL valid = [curly validateValue:&emptyName forKey:@"lastName" error:&error];
if (!valid) {
NSLog(@"Error:%@",([error userInfo])[NSLocalizedDescriptionKey]);
}
}
return 0;
}
運行結果:
2016-07-20 17:02:59.635 Coders[23278:214550] Person first name:Curly
2016-07-20 17:02:59.636 Coders[23278:214550] Person full name:Curly Howard
2016-07-20 17:02:59.636 Coders[23278:214550]
Coder name:Curly Howard
languages:(
"Objective-C",
C
)
2016-07-20 17:02:59.637 Coders[23278:214550]
Coder name:Larry Fine
languages:(
"Objective-C",
"C++"
)
2016-07-20 17:02:59.637 Coders[23278:214550] Value changed for Person object, key path: fullName, new value :Curly Fine
2016-07-20 17:02:59.637 Coders[23278:214550] Number of coders = 2
2016-07-20 17:02:59.637 Coders[23278:214550] Error:Last name cannot be nil