KVC(Key-value coding)键值编码: 指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。(一个非正式的Protocol,提供一种机制来间接访问对象的属性。而不是通过调用Setter、Getter方法访问。)这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定,这也是iOS开发中的黑魔法之一。
KVC最为重要的四个方法:
-
(
nullable
id
)
valueForKey
:
(
NSString *
)
key
;
//直接通过Key来取值
-
(
void
)
setValue
:
(
nullable
id
)
value
forKey
:
(
NSString *
)
key
;
//通过Key来设值
-
(
nullable
id
)
valueForKeyPath
:
(
NSString *
)
keyPath
;
//通过KeyPath来取值
-
(
void
)
setValue
:
(
nullable
id
)
value
forKeyPath
:
(
NSString *
)
keyPath
;
//通过KeyPath来设值
NSKeyValueCoding类别中其他的方法:
+
(
BOOL
)
accessInstanceVariablesDirectly
;
//默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
-
(
BOOL
)
validateValue
:
(
inout
id
__nullable *
__nonnull
)
ioValue
forKey
:
(
NSString *
)
inKey
error
:
(
out
NSError *
*
)
outError
;
//KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
-
(
NSMutableArray *
)
mutableArrayValueForKey
:
(
NSString *
)
key
;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
-
(
nullable
id
)
valueForUndefinedKey
:
(
NSString *
)
key
;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
-
(
void
)
setValue
:
(
nullable
id
)
value
forUndefinedKey
:
(
NSString *
)
key
;
//和上一个方法一样,只不过是设值。
-
(
void
)
setNilValueForKey
:
(
NSString *
)
key
;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
-
(
NSDictionary *
)
dictionaryWithValuesForKeys
:
(
NSArray *
)
keys
;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
在KVC中使用KeyPath
然而在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来再该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。
-
(
nullable
id
)
valueForKeyPath
:
(
NSString *
)
keyPath
;
//通过KeyPath来取值
-
(
void
)
setValue
:
(
nullable
id
)
value
forKeyPath
:
(
NSString *
)
keyPath
;
//通过KeyPath来设值
例子:
@
interface
Address
:
NSObject
@
end
@
interface
Address
(
)
@
property
(
nonatomic
,
copy
)
NSString*
country
;
@
end
@
implementation
Address
@
end
@
interface
People
:
NSObject
@
end
@
interface
People
(
)
@
property
(
nonatomic
,
copy
)
NSString*
name
;
@
property
(
nonatomic
,
strong
)
Address*
address
;
@
property
(
nonatomic
,
assign
)
NSInteger
age
;
@
end
@
implementation
People
@
end
int
main
(
int
argc
,
const
char
*
argv
[
]
)
{
@
autoreleasepool
{
People*
people1
=
[
People
new
]
;
Address*
add
=
[
Address
new
]
;
add
.
country
=
@
"China"
;
people1
.
address
=
add
;
NSString*
country1
=
people1
.
address
.
country
;
NSString *
country2
=
[
people1
valueForKeyPath
:
@
"address.country"
]
;
NSLog
(
@
"country1:%@ country2:%@"
,
country1
,
country2
)
;
[
people1
setValue
:
@
"USA"
forKeyPath
:
@
"address.country"
]
;
country1
=
people1
.
address
.
country
;
country2
=
[
people1
valueForKeyPath
:
@
"address.country"
]
;
NSLog
(
@
"country1:%@ country2:%@"
,
country1
,
country2
)
;
}
return
0
;
}
//打印结果
2016
-
04
-
17
15
:
55
:
22.487
KVCDemo
[
1190
:
82636
]
country1
:
China
country2
:
China
2016
-
04
-
17
15
:
55
:
22.489
KVCDemo
[
1190
:
82636
]
country1
:
USA
country2
:
USA
上面的代码简单在展示了KeyPath是怎么用的。如果你不小心错误的使用了key而非KeyPath的话,KVC会直接查找
address.country
这个属性,很明显,这个属性并不存在,所以会再调用
UndefinedKey
相关方法。而KVC对于KeyPath是搜索机制第一步就是分离key,用小数点
.
来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。
KVC如何处理异常
KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
如果你不小心传了nil,KVC会调用
setNilValueForKey:
方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
[
people1
setValue
:
nil
forKey
:
@
"age"
]
*
*
*
Terminating
app
due
to
uncaught
exception
'NSInvalidArgumentException'
,
reason
:
'[ setNilValueForKey]: could not set nil as the value for the key age.'
// 调用setNilValueForKey抛出异常
如果重写
setNilValueForKey:
就没问题了
@
implementation
People
-
(
void
)
setNilValueForKey
:
(
NSString *
)
key
{
NSLog
(
@
"不能将%@设成nil"
,
key
)
;
}
@
end
//打印出
2016
-
04
-
17
16
:
19
:
55.298
KVCDemo
[
1304
:
92472
]
不能将
age设成
nil
KVC处理非对象和自定义对象
不是每一个方法都返回对象,但是valueForKey:
总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开以者需要手动转换成原来的类型。尽管valueForKey:
会自动将值类型封装成对象,但是setValue:forKey:
却不行。你必须手动将值类型转换成NSNumber
或者NSValue
类型,才能传递过去。
对于自定义对象,KVC也会正确以设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。
KVC与容器类
@
interface
demo
:
NSObject
@
property
(
nonatomic
,
strong
)
NSMutableArray*
arr
;
@
end
@
implementation
demo
-
(
id
)
init
{
if
(
self
==
[
super
init
]
)
{
_arr
=
[
NSMutableArray
new
]
;
[
self
addObserver
:
self
forKeyPath
:
@
"arr"
options
:
NSKeyValueObservingOptionNew
|
NSKeyValueObservingOptionOld
context
:
nil
]
;
}
return
self
;
}
-
(
void
)
observeValueForKeyPath
:
(
NSString *
)
keyPath
ofObject
:
(
id
)
object
change
:
(
NSDictionary *
)
change
context
:
(
void
*
)
context
{
NSLog
(
@
"%@"
,
change
)
;
}
-
(
void
)
dealloc
{
[
self
removeObserver
:
self
forKeyPath
:
@
"arr"
]
;
//一定要在dealloc里面移除观察
}
-
(
void
)
addItem
{
[
_arr
addObject
:
@
"1"
]
;
}
-
(
void
)
addItemObserver
{
[
[
self
mutableArrayValueForKey
:
@
"arr"
]
addObject
:
@
"1"
]
;
}
-
(
void
)
removeItemObserver
{
[
[
self
mutableArrayValueForKey
:
@
"arr"
]
removeLastObject
]
;
}
@
end
然后再
:
demo*
d
=
[
demo
new
]
;
[
d
addItem
]
;
[
d
addItemObserver
]
;
[
d
removeItemObserver
]
;
打印结果
2016
-
04
-
18
17
:
48
:
22.675
KVCDemo
[
32647
:
505864
]
{
indexes
=
"[number of indexes: 1 (in 1 ranges), indexes: (1)]"
;
kind
=
2
;
new
=
(
1
)
;
}
2016
-
04
-
18
17
:
48
:
22.677
KVCDemo
[
32647
:
505864
]
{
indexes
=
"[number of indexes: 1 (in 1 ranges), indexes: (1)]"
;
kind
=
3
;
old
=
(
1
)
;
}
从上面的代码可以看出,当只是普通地调用[_arr addObject:@"1"]
时,Observer并不会回调,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"1"]
;这样写时才能正确地触发KVO。打印出来的数据中,可以看出这次操作的详情,kind可能是指操作方法(我还不是很确认),old和new并不是成对出现的,当加添新数据时是new,删除数据时是old
而对于无序的容器,可以用下面的方法:
-
(
NSMutableSet *
)
mutableSetValueForKey
:
(
NSString *
)
key
;
同样,它们也有对应的keyPath版本
-
(
NSMutableArray *
)
mutableArrayValueForKeyPath
:
(
NSString *
)
keyPath
;
-
(
NSMutableSet *
)
mutableSetValueForKeyPath
:
(
NSString *
)
keyPath
;
KVC和字典
当对NSDictionary对象使用KVC时,valueForKey:
的表现行为和objectForKey:
一样。所以使用valueForKeyPath:
用来访问多层嵌套的字典是比较方便的。
KVC里面还有两个关于NSDictionary的方法
-
(
NSDictionary *
)
dictionaryWithValuesForKeys
:
(
NSArray *
)
keys
;
-
(
void
)
setValuesForKeysWithDictionary
:
(
NSDictionary *
)
keyedValues
;
dictionaryWithValuesForKeys:
是指输入一组key,返回这组key对应的属性,再组成一个字典。
setValuesForKeysWithDictionary
是用来修改Model中对应key的属性。下面直接用代码会更直观一点
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
)
;
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
)
;
//打印结果
2016
-
04
-
19
11
:
54
:
30.846
KVCDemo
[
6607
:
198900
]
{
city
=
"Shen Zhen"
;
country
=
China
;
district
=
"Nan Shan"
;
province
=
"Guang Dong"
;
}
2016
-
04
-
19
11
:
54
:
30.847
KVCDemo
[
6607
:
198900
]
country
:
USA
province
:
california
city
:
Los
angle
KVC的使用
KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,KVC也是许多iOS开发黑魔法的基础。下面我来列举iOS开发中KVC的使用场景
动态地取值和设值
利用KVC动态的取值和设值是最基本的用途了。相信每一个iOS开发者都能熟练掌握,
用KVC来访问和修改私有变量
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的,请参考本文前面的Dog类的例子。
Model和字典转换
- -(id) initWithDictionary:(NSMutableDictionary*) jsonObject
- {
- if((self = [super init]))
- {
- [self init];
- [self setValuesForKeysWithDictionary:jsonObject];
- }
- return self;
- - (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];
- }
修改一些控件的内部属性
这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些空间的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。
下面演示如果修改placeHolder的文字样式。这里的关键点是如果获取你要修改的样式的属性名,也就是key或者keyPath名。