iOS KVC & KVO
2014-05-17 11:19
351 查看
Key Value Coding
Key Value Coding是cocoa的一个标准组成部分,它能让我们可以通过name(key)的方式访问property, 不必调用明确的property accssor, 如我们有个property叫做foo, 我们可以foo直接访问它,同样我们也可以用KVC来完成[Object valueForKey:@“foo”], 有同学就会问了, 这样做有什么好处呢?主要的好处就是来减少我们的代码量。
下面我们来看看几个例子,就明白了KVO的用法和好处了,假设这样个类叫做People,
@interface People: NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@end
场景1,apple 官网的一个例子,当我们需要统计很多People的时候,每一行是一个人的实例,并且有2列属性,name, age, 这时候我们可以会这样做,
- (id)tableView:(NSTableView *)tableview
objectValueForTableColumn:(id)column row:(NSInteger)row {
People *people = [peoleArray objectAtIndex:row];
if ([[column identifier] isEqualToString:@"name"]) {
return [people name];
}
if ([[column identifier] isEqualToString:@"age"]) {
return [people age];
}
// And so on.
}
同样我们也可以用KVC,帮助我们化简这些if, 因为name, age其实都是property, 我们可以直接通过key来访问,所以整理过后是
People *people = [peopleArray objectAtIndex:row];
return [people valueForKey:[column identifier]];
场景2,这下我们有了server, server的某个api(listPeople??), 会返回我们json格式一个数组,里面包含这样dict{name:xx, age:xx}这样的数据, 我们希望用这些dict数据构造出我们的people来,通常我们的做法是,为我们People类写一个static factory方法专门用来处理dict来, 把dict里面的数据取出来, 然后创建个空的People对象,然后依次设置property。然而当这样类似People的与server交互的类多了,我们就要为每个类都要加上这样的wrapper,
是否有种简单办法来设置这样的属性,当然就是我们的KVC了。
-(id) initWithDictionary:(NSMutableDictionary*) jsonObject
{
if((self = [super init]))
{
[self init];
[self setValuesForKeysWithDictionary:jsonObject];
}
return self;
}
setValuesForKeysWithDictionary, 会为我们把和dictionary的key名字相同的class proerty设置上dict中key对应的value, 是不是很方便呀,但是有同学又要问了 如果json里面的某些key就是和object的property名字不一样呢,或者有些server返回的字段是objc保留字如”id”, “description”等, 我们也希望也map dict to object, 这时候我们就需要用上setValue:forUndefinedKey,
因为如果我们不处理这些Undefined Key,还是用setValuesForKeysWithDictionary就会 抛出异常。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if([key isEqualToString:@"nameXXX"])
self.name = value;
if([key isEqualToString:@"ageXXX"])
self.age = value;
else
[super setValue:value forKey:key];
}
所以只要重载这个方法,就可以处理了那些无法跟property相匹配的key了,默认的实现是抛出一个NSUndefinedKeyException,又有同学发问了如果 这时候server返回的People有了内嵌的json(如Products{product1{count:xx, sumPrice:xx}}, product2{} ….),又该怎么办,能把这个内嵌的json转化成我们的客户端的Product类嘛, 当然可以这时候就需要重载setValue:forKey,
单独处理”Products”这个key, 把它wrapper成我们需要的class
-(void) setValue:(id)value forKey:(NSString *)key
{
if([key isEqualToString:@"products"])
{
for(NSMutableDictionary *productDict in value)
{
Prodcut *product = [[Product alloc] initWithDictionary:prodcutDict];
[self.products addObject:product];
}
}
}
场景3,我们需要把一个数组里的People的名字的首字母大写,并且把新的名字存入新的数组, 这时候通常做法会是遍历整个数组,然后把每个People的name取出来,调用 capitalizedString 然后把新的String加入新的数组中。 有了KVC就有了新做法:
[array valueForKeyPath:@"name.capitalizedString"]
我们看到valueForKeyPath, 为什么用valueForKeyPath, 不用valueForKey, 因为valueForKeyPath可以传递关系,例如这里是每个People的name property的String的capitalizedString property, 而valueForKey不能传递这样的关系,所以对于dict里面的dict, 我们也只能用valueForKeyPath。这里我们也看到KVC对于array(set),
做了特殊处理,不是简单操作collection上,而是 针对这些collection里面的元素进行操作,同样KVC也提供更多地操作,例如@sum这些针对collection,有兴趣的同学可以去用下。
场景4,当我们执行NSArray *products = [people valueForKey:@“products”],我们希望的是[people products],可是people没有这样的方法, KVC又会为我们带来些什么呢?
首先会去找getProdcuts or products or isProducts, 按照这样的顺序去查找,第一个找到的就返回
然后会去找countOfProducts and either objectInProductsAtIndex: or ProductsAtIndexes, 如果找到,就会去找countOfProducts and enumeratorOfProducts and memberOfProducts 这个2个方法都找到了,KVC才会给我们返回一个代理的NSKeyValueArray,用于我们后续的操作(addProduct之类的)。
如果有个变量叫做 products, isProducts, products or isProducts, KVC会直接就使用这样的变量,如果你觉得直接用这样的变量是破坏了封装, 可以禁止这样的行为发生,重载 +accessInstanceVariablesDirectly,返回NO。
简单来说,valueForKey, 会给我们带来一个代理array, 如果我们实现了某些方法,上诉的这些方法只是针对NSArray, 对于mutable的collection, 我们还需要提供其他 方法的实现才行。
Key Value Observing
Key Value Observing, 顾名思义就是一种observer 模式用于监听property的变化,KVO跟NSNotification有很多相似的地方, 用addObserver:forKeyPath:options:context:去start observer, 用removeObserver:forKeyPath:context去stop observer, 回调就是observeValueForKeyPath:ofObject:change:context:。
- (void)removeObservation {
[self.object removeObserver:self
forKeyPath:self.property];
}
- (void)addObservation {
[self.object addObserver:self forKeyPath:self.property
options:0
context:(__bridge void*)self];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ((__bridge id)context == self) {
// 只处理跟我们当前class的property更新
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
对于KVO来说,我们要做的只是简单update 我们的property数据,不需要像NSNotificationCenter那样关心是否有人在监听你的请求,如果没有人监听该怎么办, 所有addObserver, removeObserver, callback 都是想要监听的你的property的class做的事情。 曾经做个项目,用NSNotificationCenter post Notification在一个network callback里面,可是这时候因为最早的addObserver的class被释放了,
接着生成的addObserver的class, 就接受到了上一个observer该监听的事件,所以造成了错误,那时候的解决方案是为addObserve key做unique,不会2次addObserver 的key是相同的,但是有了KVO, 我们同样可以用KVO来完成,当addOberver的的object remove的时候,就不会有这样的callback被调用了。
KVO给我们提供了更少的代码,和比NSNotification好处,不需要修改被观察的class, 永远都是观察你的人做事情。 但是KVO也有些毛病, 1. 如果没有observer监听key path, removeObsever:forKeyPath:context: 这个key path, 就会crash, 不像NSNotificationCenter removeObserver。 2. 对代码你很难发现谁监听你的property的改动,查找起来比较麻烦。
3. 对于一个复杂和相关性很高的class,最好还是不要用KVO, 就用delegate 或者 notification的方式比较简洁。
Summary
尽量使用KVC可以大大地减少我们的代码量,当遇到property的时候,可以多想想是否可以KVC来帮助我,是否可以用KVC来重构代码, 当需要加入observer模式时,可以考虑下KVO, 在高性能的observer里面,KVO会给我们很好的帮助。
#import "AppDelegate.h"
#import "Person.h"
#import "Card.h"
#import "Book.h"
@interface
AppDelegate()
@property (strong,
nonatomic) Person *myPerson;
@end
@implementation AppDelegate
/**
KVC - Key value coding
键值编码
1. 使用kvc为对象赋值或者取值时,需要知道准确的键值
2. 相比较点语法,kvc是一种间接的传递方式,这种方式有利于对象解耦,让对象彼此之间的耦合度不要太高
3. 赋值语句
1> setValue:forKey:
给对象当前的属性赋值
2> setValue:forKeyPath:
按照对象的层级关系为其中的属性赋值
提示:forKeyPath可以替代forKey,但是forKey不能替代forKeyPath
3> setValuesForKeysWithDictionary:
可以从plist文件中读取对应的数据字典,对对象属性赋值
4. KVO - Key value Observer(观察者)
1> 注册观察者
addObserver:forKeyPath:options:context:
2> 观察者对象需要实现以下方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
3> 注意事项,不能观察已经被释放的对象,如果要观察,需要是强应用对象,或者被其他对象强应用的对象
4> observeValueForKeyPath
方法是NSObject的分类,意味着可以观察任何对象。
*/
- (void)initPerson:(id)person
{
[person setValue:@"jake"
forKeyPath:@"name"];
//
提示:在赋值时将NSNumber赋给对应的键值,之后会自动转换成NSInteger
[person setValue:@25
forKeyPath:@"age"];
}
#pragma mark - KVC简单演练1
- (void)kvcDemo1
{
Person *person = [[Person
alloc]init];
[self
initPerson:person];
person.card = [[Card
alloc]init];
// person.card.no = @"123";
[person setValue:@"321"
forKeyPath:@"card.no"];
Book *book1 = [[Book
alloc]init];
book1.bookName =
@"ios 1";
book1.price =
28.98;
Book *book2 = [[Book
alloc]init];
book2.bookName =
@"ios 2";
book2.price =
38.98;
person.books =
@[book1, book2];
NSLog(@"%@", person);
NSLog(@"%@", [person
valueForKeyPath:@"books.price"]);
}
#pragma mark - KVC绑定Plist文件
- (void)kvcDemo2
{
//
读取plist文件
// 1.
路径
NSString *path = [[NSBundle
mainBundle]pathForResource:@"Person"
ofType:@"plist"];
NSArray *array = [NSArray
arrayWithContentsOfFile:path];
NSMutableArray *arrayM = [NSMutableArray
arrayWithCapacity:array.count];
for (NSDictionary *dict
in array) {
Person *p = [[Person
alloc]init];
//
初始化person的方法
[p setValuesForKeysWithDictionary:dict];
[arrayM
addObject:p];
}
NSLog(@"%@", arrayM);
NSLog(@"%@", [arrayM
valueForKeyPath:@"books.bookName"]);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions
{
Person *p = [[Person
alloc]init];
p.name =
@"mary";
self.myPerson = p;
// 如果用户的姓名发生改变,需要及时得到通知
// 如果要监听对象属性的变化,除了系统的通知中心之外,我们也可以自己注册观察者
// 提示:观察者模式的性能不好,使用完毕后,一定记住释放观察者
// addObserver
的参数:
// 1. 观察者对象,可以是自定义对象,也可以是self
// 2.
观察对象的键值路径
// 3.
观察数值变化情况选项
// NSKeyValueObservingOptionNew
只观察新变化的值
// NSKeyValueObservingOptionOld
变化之前的数值
// 4. context
指定观察者的同时,可以通过context设置字符串参数标示
[p addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:@"hello"];
p.age =
20;
p.name =
@"mike";
//
删除观察者
[p removeObserver:self
forKeyPath:@"name"];
p.name =
@"jake";
self.window = [[UIWindow
alloc] initWithFrame:[[UIScreen
mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor
= [UIColor whiteColor];
[self.window
makeKeyAndVisible];
return
YES;
}
/**
1. keyPath
键值路径
2. object
被观察的对象
3. change
键值变化情况字典
4. context 指定观察者时,设置的字符串参数
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@ %@ %@ %@", keyPath, object, change, context);
}
Key Value Coding是cocoa的一个标准组成部分,它能让我们可以通过name(key)的方式访问property, 不必调用明确的property accssor, 如我们有个property叫做foo, 我们可以foo直接访问它,同样我们也可以用KVC来完成[Object valueForKey:@“foo”], 有同学就会问了, 这样做有什么好处呢?主要的好处就是来减少我们的代码量。
下面我们来看看几个例子,就明白了KVO的用法和好处了,假设这样个类叫做People,
@interface People: NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@end
场景1,apple 官网的一个例子,当我们需要统计很多People的时候,每一行是一个人的实例,并且有2列属性,name, age, 这时候我们可以会这样做,
- (id)tableView:(NSTableView *)tableview
objectValueForTableColumn:(id)column row:(NSInteger)row {
People *people = [peoleArray objectAtIndex:row];
if ([[column identifier] isEqualToString:@"name"]) {
return [people name];
}
if ([[column identifier] isEqualToString:@"age"]) {
return [people age];
}
// And so on.
}
同样我们也可以用KVC,帮助我们化简这些if, 因为name, age其实都是property, 我们可以直接通过key来访问,所以整理过后是
People *people = [peopleArray objectAtIndex:row];
return [people valueForKey:[column identifier]];
场景2,这下我们有了server, server的某个api(listPeople??), 会返回我们json格式一个数组,里面包含这样dict{name:xx, age:xx}这样的数据, 我们希望用这些dict数据构造出我们的people来,通常我们的做法是,为我们People类写一个static factory方法专门用来处理dict来, 把dict里面的数据取出来, 然后创建个空的People对象,然后依次设置property。然而当这样类似People的与server交互的类多了,我们就要为每个类都要加上这样的wrapper,
是否有种简单办法来设置这样的属性,当然就是我们的KVC了。
-(id) initWithDictionary:(NSMutableDictionary*) jsonObject
{
if((self = [super init]))
{
[self init];
[self setValuesForKeysWithDictionary:jsonObject];
}
return self;
}
setValuesForKeysWithDictionary, 会为我们把和dictionary的key名字相同的class proerty设置上dict中key对应的value, 是不是很方便呀,但是有同学又要问了 如果json里面的某些key就是和object的property名字不一样呢,或者有些server返回的字段是objc保留字如”id”, “description”等, 我们也希望也map dict to object, 这时候我们就需要用上setValue:forUndefinedKey,
因为如果我们不处理这些Undefined Key,还是用setValuesForKeysWithDictionary就会 抛出异常。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if([key isEqualToString:@"nameXXX"])
self.name = value;
if([key isEqualToString:@"ageXXX"])
self.age = value;
else
[super setValue:value forKey:key];
}
所以只要重载这个方法,就可以处理了那些无法跟property相匹配的key了,默认的实现是抛出一个NSUndefinedKeyException,又有同学发问了如果 这时候server返回的People有了内嵌的json(如Products{product1{count:xx, sumPrice:xx}}, product2{} ….),又该怎么办,能把这个内嵌的json转化成我们的客户端的Product类嘛, 当然可以这时候就需要重载setValue:forKey,
单独处理”Products”这个key, 把它wrapper成我们需要的class
-(void) setValue:(id)value forKey:(NSString *)key
{
if([key isEqualToString:@"products"])
{
for(NSMutableDictionary *productDict in value)
{
Prodcut *product = [[Product alloc] initWithDictionary:prodcutDict];
[self.products addObject:product];
}
}
}
场景3,我们需要把一个数组里的People的名字的首字母大写,并且把新的名字存入新的数组, 这时候通常做法会是遍历整个数组,然后把每个People的name取出来,调用 capitalizedString 然后把新的String加入新的数组中。 有了KVC就有了新做法:
[array valueForKeyPath:@"name.capitalizedString"]
我们看到valueForKeyPath, 为什么用valueForKeyPath, 不用valueForKey, 因为valueForKeyPath可以传递关系,例如这里是每个People的name property的String的capitalizedString property, 而valueForKey不能传递这样的关系,所以对于dict里面的dict, 我们也只能用valueForKeyPath。这里我们也看到KVC对于array(set),
做了特殊处理,不是简单操作collection上,而是 针对这些collection里面的元素进行操作,同样KVC也提供更多地操作,例如@sum这些针对collection,有兴趣的同学可以去用下。
场景4,当我们执行NSArray *products = [people valueForKey:@“products”],我们希望的是[people products],可是people没有这样的方法, KVC又会为我们带来些什么呢?
首先会去找getProdcuts or products or isProducts, 按照这样的顺序去查找,第一个找到的就返回
然后会去找countOfProducts and either objectInProductsAtIndex: or ProductsAtIndexes, 如果找到,就会去找countOfProducts and enumeratorOfProducts and memberOfProducts 这个2个方法都找到了,KVC才会给我们返回一个代理的NSKeyValueArray,用于我们后续的操作(addProduct之类的)。
如果有个变量叫做 products, isProducts, products or isProducts, KVC会直接就使用这样的变量,如果你觉得直接用这样的变量是破坏了封装, 可以禁止这样的行为发生,重载 +accessInstanceVariablesDirectly,返回NO。
简单来说,valueForKey, 会给我们带来一个代理array, 如果我们实现了某些方法,上诉的这些方法只是针对NSArray, 对于mutable的collection, 我们还需要提供其他 方法的实现才行。
Key Value Observing
Key Value Observing, 顾名思义就是一种observer 模式用于监听property的变化,KVO跟NSNotification有很多相似的地方, 用addObserver:forKeyPath:options:context:去start observer, 用removeObserver:forKeyPath:context去stop observer, 回调就是observeValueForKeyPath:ofObject:change:context:。
- (void)removeObservation {
[self.object removeObserver:self
forKeyPath:self.property];
}
- (void)addObservation {
[self.object addObserver:self forKeyPath:self.property
options:0
context:(__bridge void*)self];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ((__bridge id)context == self) {
// 只处理跟我们当前class的property更新
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
对于KVO来说,我们要做的只是简单update 我们的property数据,不需要像NSNotificationCenter那样关心是否有人在监听你的请求,如果没有人监听该怎么办, 所有addObserver, removeObserver, callback 都是想要监听的你的property的class做的事情。 曾经做个项目,用NSNotificationCenter post Notification在一个network callback里面,可是这时候因为最早的addObserver的class被释放了,
接着生成的addObserver的class, 就接受到了上一个observer该监听的事件,所以造成了错误,那时候的解决方案是为addObserve key做unique,不会2次addObserver 的key是相同的,但是有了KVO, 我们同样可以用KVO来完成,当addOberver的的object remove的时候,就不会有这样的callback被调用了。
KVO给我们提供了更少的代码,和比NSNotification好处,不需要修改被观察的class, 永远都是观察你的人做事情。 但是KVO也有些毛病, 1. 如果没有observer监听key path, removeObsever:forKeyPath:context: 这个key path, 就会crash, 不像NSNotificationCenter removeObserver。 2. 对代码你很难发现谁监听你的property的改动,查找起来比较麻烦。
3. 对于一个复杂和相关性很高的class,最好还是不要用KVO, 就用delegate 或者 notification的方式比较简洁。
Summary
尽量使用KVC可以大大地减少我们的代码量,当遇到property的时候,可以多想想是否可以KVC来帮助我,是否可以用KVC来重构代码, 当需要加入observer模式时,可以考虑下KVO, 在高性能的observer里面,KVO会给我们很好的帮助。
#import "AppDelegate.h"
#import "Person.h"
#import "Card.h"
#import "Book.h"
@interface
AppDelegate()
@property (strong,
nonatomic) Person *myPerson;
@end
@implementation AppDelegate
/**
KVC - Key value coding
键值编码
1. 使用kvc为对象赋值或者取值时,需要知道准确的键值
2. 相比较点语法,kvc是一种间接的传递方式,这种方式有利于对象解耦,让对象彼此之间的耦合度不要太高
3. 赋值语句
1> setValue:forKey:
给对象当前的属性赋值
2> setValue:forKeyPath:
按照对象的层级关系为其中的属性赋值
提示:forKeyPath可以替代forKey,但是forKey不能替代forKeyPath
3> setValuesForKeysWithDictionary:
可以从plist文件中读取对应的数据字典,对对象属性赋值
4. KVO - Key value Observer(观察者)
1> 注册观察者
addObserver:forKeyPath:options:context:
2> 观察者对象需要实现以下方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
3> 注意事项,不能观察已经被释放的对象,如果要观察,需要是强应用对象,或者被其他对象强应用的对象
4> observeValueForKeyPath
方法是NSObject的分类,意味着可以观察任何对象。
*/
- (void)initPerson:(id)person
{
[person setValue:@"jake"
forKeyPath:@"name"];
//
提示:在赋值时将NSNumber赋给对应的键值,之后会自动转换成NSInteger
[person setValue:@25
forKeyPath:@"age"];
}
#pragma mark - KVC简单演练1
- (void)kvcDemo1
{
Person *person = [[Person
alloc]init];
[self
initPerson:person];
person.card = [[Card
alloc]init];
// person.card.no = @"123";
[person setValue:@"321"
forKeyPath:@"card.no"];
Book *book1 = [[Book
alloc]init];
book1.bookName =
@"ios 1";
book1.price =
28.98;
Book *book2 = [[Book
alloc]init];
book2.bookName =
@"ios 2";
book2.price =
38.98;
person.books =
@[book1, book2];
NSLog(@"%@", person);
NSLog(@"%@", [person
valueForKeyPath:@"books.price"]);
}
#pragma mark - KVC绑定Plist文件
- (void)kvcDemo2
{
//
读取plist文件
// 1.
路径
NSString *path = [[NSBundle
mainBundle]pathForResource:@"Person"
ofType:@"plist"];
NSArray *array = [NSArray
arrayWithContentsOfFile:path];
NSMutableArray *arrayM = [NSMutableArray
arrayWithCapacity:array.count];
for (NSDictionary *dict
in array) {
Person *p = [[Person
alloc]init];
//
初始化person的方法
[p setValuesForKeysWithDictionary:dict];
[arrayM
addObject:p];
}
NSLog(@"%@", arrayM);
NSLog(@"%@", [arrayM
valueForKeyPath:@"books.bookName"]);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions
{
Person *p = [[Person
alloc]init];
p.name =
@"mary";
self.myPerson = p;
// 如果用户的姓名发生改变,需要及时得到通知
// 如果要监听对象属性的变化,除了系统的通知中心之外,我们也可以自己注册观察者
// 提示:观察者模式的性能不好,使用完毕后,一定记住释放观察者
// addObserver
的参数:
// 1. 观察者对象,可以是自定义对象,也可以是self
// 2.
观察对象的键值路径
// 3.
观察数值变化情况选项
// NSKeyValueObservingOptionNew
只观察新变化的值
// NSKeyValueObservingOptionOld
变化之前的数值
// 4. context
指定观察者的同时,可以通过context设置字符串参数标示
[p addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:@"hello"];
p.age =
20;
p.name =
@"mike";
//
删除观察者
[p removeObserver:self
forKeyPath:@"name"];
p.name =
@"jake";
self.window = [[UIWindow
alloc] initWithFrame:[[UIScreen
mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor
= [UIColor whiteColor];
[self.window
makeKeyAndVisible];
return
YES;
}
/**
1. keyPath
键值路径
2. object
被观察的对象
3. change
键值变化情况字典
4. context 指定观察者时,设置的字符串参数
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@ %@ %@ %@", keyPath, object, change, context);
}
相关文章推荐
- iOS KVO & KVC
- iOS KVO & KVC
- iOS KVC & KVO
- iOS KVC & KVO
- iOS开发-Day24-OC KVC&KVO&通知
- 【IOS】KVC(Key - value coding 机制) & KVO(键值观察)
- iOS KVC & KVO
- 【iOS开发系列】KVC&KVO
- iOS开发中KVO & KVC的使用
- iOS 观察者模式(KVC&KVO、通知)
- iOS KVC & KVO
- iOS KVO & KVC
- iOS KVC & KVO
- iOS开发--KVC & KVO
- ios KVO&KVC
- iOS KVC & KVO
- iOS的观察者模式之:KVC&KVO
- IOS 之 KVC & KVO
- iOS KVC & KVO
- iOS开发_kvc&kvo