[精通Objective-C]键值编程

[精通Objective-C]键值编程

参考书籍:《精通Objective-C》【美】 Keith Lee

目录

键值编码KVC

键值编码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<key>:error:,其中是属性名称。

@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 <Foundation/Foundation.h>

@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 <Foundation/Foundation.h>

@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<NSString *,id> *)change context:(void *)context{
    NSString *newValue = change[NSKeyValueChangeNewKey];
    NSLog(@"Value changed for %@ object, key path: %@, new value :%@", [object className], keyPath, newValue);
}
@end

再创建一个Coders类,测试无序一对多关系属性:

#import <Foundation/Foundation.h>

@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 <Foundation/Foundation.h>
#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
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值