1. KVC 用來(lái)做什么?
和對(duì)象的點(diǎn)語(yǔ)法類(lèi)似勺鸦,都是用來(lái)賦值和取值的并巍。
2. 為什么要用 KVC?
通常祝旷,兩種情況下我們需要重新審視自己的賦值方式履澳。
- 訪(fǎng)問(wèn)私有屬性/變量。正常的情況下怀跛,我們根據(jù)設(shè)計(jì)原型距贷,開(kāi)開(kāi)心心的搭建面,初始化各種控件吻谋,
UIView / UIButton / UILabel
忠蝗,然后設(shè)置其相應(yīng)的屬性,以使界面滿(mǎn)足我們的需求漓拾。假如界面很簡(jiǎn)單阁最,效果不是很炫酷,那我們只需要通過(guò)設(shè)置子控件的各種屬性骇两,調(diào)整背景顏色速种,切個(gè)圓角,改變透明度等等低千,就可以將界面搞定配阵。可是,有些效果棋傍,偏偏是通過(guò)蘋(píng)果暴露給我們的接口不能滿(mǎn)足的救拉,比如,修改文字輸入框(UITextField)的占位文字的顏色瘫拣,這個(gè)時(shí)候怎么辦亿絮?想要textField.placeholederLabel.textColor = [UIColor redColor]
沒(méi)有這個(gè)屬性啊,怎么實(shí)現(xiàn)麸拄?另外還有一些只讀屬性派昧,無(wú)法通過(guò)點(diǎn)語(yǔ)法正常賦值,怎么辦感帅? - 請(qǐng)求服務(wù)器數(shù)據(jù)之后的處理斗锭。沒(méi)有KVC,我們?nèi)绻虢o某個(gè)屬性賦值失球,則需要通過(guò)
object.property = value
; 這種方式進(jìn)行賦值。試想一下帮毁,如果是一個(gè)對(duì)象的幾個(gè)屬性這樣設(shè)置還行得通实苞,可是通常客戶(hù)端的數(shù)據(jù)都是從服務(wù)器端獲取烈疚,格式一般是 json/xml黔牵,我們需要通過(guò)字典轉(zhuǎn)模型,將字符串化的鍵值對(duì)轉(zhuǎn)換成對(duì)象的屬性值進(jìn)行保存和使用爷肝,通常這種數(shù)據(jù)量是很大的猾浦,而且還是帶各種嵌套的,難道這個(gè)時(shí)候我們還要先創(chuàng)建各種對(duì)象灯抛,然后根據(jù)鍵值對(duì)的鍵金赦,創(chuàng)建對(duì)應(yīng)的屬性,之后再手動(dòng)一個(gè)個(gè)model.property = value;
保存嗎对嚼?肯定是不現(xiàn)實(shí)的夹抗。那怎么辦?
3. 有了 KVC 之后纵竖,上述問(wèn)題怎么解決漠烧?
-
[textField setValue:[UIColor purpleColor] forKeyPath:@"_placeholderLabel.textColor"];
當(dāng)然,這里邊靡砌,找到正確的沒(méi)有暴露的接口也是個(gè)問(wèn)題已脓,以后再說(shuō)吧,大致就是利用 Runtime 通殃,給程序打斷點(diǎn)度液,然后將 UITextFiled 的所有變量(包括未暴露的私有變量)打印,之后根據(jù)名字猜測(cè)+嘗試+運(yùn)氣通常就能找到我們需要修改的屬性了。
這里為什么要使用setValue: forKeyPath:
而不是setValue: forKey:
呢恨诱?因?yàn)?_placeholederLabel
是UITextField
的屬性媳瞪,而我們要設(shè)置的是_placeholederLabel
的屬性,像這種有多層屬性嵌套的情況就需要使用setValue: forKeyPath:
照宝。
經(jīng)過(guò)上邊一行簡(jiǎn)單的代碼我們就可以很優(yōu)雅的實(shí)現(xiàn)修改占位文字的顏色問(wèn)題了蛇受。這里需要說(shuō)明的一點(diǎn)是:占位文字的顏色和大小還可以通過(guò)設(shè)置attributedPlaceholder
屬性設(shè)置,也是比較方便的厕鹃。代碼如下:
NSString *placeholder = @"我是占位文字";
NSDictionary *attr = @{NSForegroundColorAttributeName : [UIColor orangeColor],
NSFontAttributeName : [UIFont systemFontOfSize:15] };
textField.attributedPlaceholder = [[NSAttributedString alloc]initWithString:placeholder attributes:attr];
- 關(guān)于字典轉(zhuǎn)模型兢仰。這里先以很簡(jiǎn)單的單層數(shù)據(jù)為例:
然后是模型
TestModel
的頭文件:
@interface TestModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *detail;
@property (copy, nonatomic) NSString *postscript;
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
TestModel
的實(shí)現(xiàn)文件:
@implementation TestModel
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
@end
最后是ViewController
控制器中的調(diào)用代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 加載本地 json 數(shù)據(jù),正常應(yīng)是請(qǐng)求服務(wù)器
[self loadData];
}
- (void)loadData {
NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"test.json" ofType:nil];
// 2. 解析數(shù)據(jù)
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:jsonPath] options:NSJSONReadingMutableContainers error:nil];
// 3. 轉(zhuǎn)換剂碴,只要屬性定義好把将,直接調(diào)用初始化方法即可
TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
// 4. 打印驗(yàn)證
NSLog(@"name - %@, detail - %@, postscript - %@", model.name, model.detail, model.postscript);
}
@end
打印結(jié)果驗(yàn)證:
通過(guò)上邊這個(gè)十分簡(jiǎn)單的例子,我們就可以看到 KVC 在數(shù)據(jù)轉(zhuǎn)模型時(shí)的便捷了忆矛。
不過(guò)這么簡(jiǎn)單的數(shù)據(jù)實(shí)際開(kāi)發(fā)中很難遇到察蹲,這里這里只是演示一下,如果有多層嵌套的話(huà)催训,就需要在模型類(lèi)中重寫(xiě)
setValue: forKey
方法洽议,然后通過(guò)判斷key
,對(duì)嵌套的字段再進(jìn)行轉(zhuǎn)換漫拭。
最外層模型
TestModel
的頭文件:
@interface TestModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *detail;
@property (copy, nonatomic) NSString *postscript;
// 多了一個(gè)嵌套的模型屬性
@property (strong, nonatomic) NestedTestModel *nestedModel;
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
最外層模型TestModel
的實(shí)現(xiàn)文件:
@implementation TestModel
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
- (void)setValue:(id)value forKey:(NSString *)key {
// 單獨(dú)處理嵌套的 key
if ([key isEqualToString:@"nested"]) {
self.nestedModel = [[NestedTestModel alloc]initWithDict:value];
} else {
// 別忘了調(diào)用父類(lèi)的方法亚兄,否則其他不需要單獨(dú)處理的屬性就不解析了
[super setValue:value forKey:key];
}
}
@end
控制器的代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 加載本地 json 數(shù)據(jù),正常應(yīng)是請(qǐng)求服務(wù)器
[self loadData];
}
- (void)loadData {
NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"test.json" ofType:nil];
// 2. 解析數(shù)據(jù)
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:jsonPath] options:NSJSONReadingMutableContainers error:nil];
// 3. 轉(zhuǎn)換采驻,只要屬性定義好审胚,直接調(diào)用初始化方法即可
TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
// 4. 打印驗(yàn)證
NSLog(@"name - %@, detail - %@, postscript - %@, nestedName - %@, nestedDetail - %@, nestedPostscript - %@, ", model.name, model.detail, model.postscript, model.nestedModel.nestedName, model.nestedModel.nestedDetail, model.nestedModel.nestedPostscript);
}
@end
這里嵌套內(nèi)部的模型NestedTestModel
基本跟單層 JSON 中的模型的代碼一致,也就是打印驗(yàn)證那一點(diǎn)礼旅,也就不再貼代碼了膳叨。文章最底有g(shù)it地址。
其實(shí)各淀,例子中的數(shù)據(jù)解析跟實(shí)際的復(fù)雜度也差好多懒鉴,這時(shí)候我們往往會(huì)使用第三方框架,簡(jiǎn)單方便碎浇。這里推薦一個(gè)比較小眾的第三方框架:NSObject-ObjectMap临谱,可以看一下它的源文件,寫(xiě)的很規(guī)整奴璃,就一個(gè)分類(lèi)悉默,可以實(shí)現(xiàn) json/xml
到對(duì)象的轉(zhuǎn)換,當(dāng)然集合類(lèi)的屬性也可以自動(dòng)轉(zhuǎn)換(需要多寫(xiě)一點(diǎn)代碼)苟穆,里邊有好多方法用都牽扯到了 Runtime
和 OC 中的反射機(jī)制抄课。
4. KVC 這么牛掰唱星,系統(tǒng)是怎么實(shí)現(xiàn)的呢?
當(dāng)我們調(diào)用setValue: forKey:testKey
方法時(shí)跟磨,系統(tǒng)會(huì)通過(guò)以下查找順序進(jìn)行賦值:
由上圖可以看出间聊,在使用 KVC 賦值時(shí)的查找順序依次為:
setTestKey
方法 --> _testKey
--> _isTestKey
--> testKey
--> isTestKey
。
后邊尋找實(shí)例變量的過(guò)程抵拘,有一個(gè)大的前提:對(duì)象的accessInstanceVariablesDirectly
方法返回YES
哎榴,當(dāng)然默認(rèn)情況下這個(gè)方法就是返回YES
的,所以并不需要我們擔(dān)心僵蛛。如果都找不到尚蝌,那就調(diào)用 setValue:forUndefinedKey
直接拋出異常。有時(shí)候如果我們不想讓程序因?yàn)檫@個(gè)原因拋出異常充尉,可以在類(lèi)的實(shí)現(xiàn)文件中重寫(xiě)該方法即可飘言。