iOS開發(fā)之-KVC 問答

1. KVC 用來做什么?

和對象的點語法類似,都是用來賦值和取值的。

2. 為什么要用 KVC?

通常,兩種情況下我們需要重新審視自己的賦值方式。

  • 訪問私有屬性/變量。正常的情況下,我們根據(jù)設計原型,開開心心的搭建面,初始化各種控件,UIView / UIButton / UILabel,然后設置其相應的屬性,以使界面滿足我們的需求。假如界面很簡單,效果不是很炫酷,那我們只需要通過設置子控件的各種屬性,調整背景顏色,切個圓角,改變透明度等等,就可以將界面搞定??墒?,有些效果,偏偏是通過蘋果暴露給我們的接口不能滿足的,比如,修改文字輸入框(UITextField)的占位文字的顏色,這個時候怎么辦?想要 textField.placeholederLabel.textColor = [UIColor redColor] 沒有這個屬性啊,怎么實現(xiàn)?另外還有一些只讀屬性,無法通過點語法正常賦值,怎么辦?
  • 請求服務器數(shù)據(jù)之后的處理。沒有KVC,我們如果想給某個屬性賦值,則需要通過 object.property = value; 這種方式進行賦值。試想一下,如果是一個對象的幾個屬性這樣設置還行得通,可是通常客戶端的數(shù)據(jù)都是從服務器端獲取,格式一般是 json/xml,我們需要通過字典轉模型,將字符串化的鍵值對轉換成對象的屬性值進行保存和使用,通常這種數(shù)據(jù)量是很大的,而且還是帶各種嵌套的,難道這個時候我們還要先創(chuàng)建各種對象,然后根據(jù)鍵值對的鍵,創(chuàng)建對應的屬性,之后再手動一個個 model.property = value;保存嗎?肯定是不現(xiàn)實的。那怎么辦?

3. 有了 KVC 之后,上述問題怎么解決?

  • [textField setValue:[UIColor purpleColor] forKeyPath:@"_placeholderLabel.textColor"]; 當然,這里邊,找到正確的沒有暴露的接口也是個問題,以后再說吧,大致就是利用 Runtime ,給程序打斷點,然后將 UITextFiled 的所有變量(包括未暴露的私有變量)打印,之后根據(jù)名字猜測+嘗試+運氣通常就能找到我們需要修改的屬性了。
    這里為什么要使用 setValue: forKeyPath: 而不是setValue: forKey:呢?因為 _placeholederLabelUITextField的屬性,而我們要設置的是_placeholederLabel 的屬性,像這種有多層屬性嵌套的情況就需要使用 setValue: forKeyPath:
    經過上邊一行簡單的代碼我們就可以很優(yōu)雅的實現(xiàn)修改占位文字的顏色問題了。這里需要說明的一點是:占位文字的顏色和大小還可以通過設置 attributedPlaceholder 屬性設置,也是比較方便的。代碼如下:
    NSString *placeholder = @"我是占位文字";
    NSDictionary *attr = @{NSForegroundColorAttributeName : [UIColor orangeColor],
                           NSFontAttributeName : [UIFont systemFontOfSize:15] };
    textField.attributedPlaceholder = [[NSAttributedString alloc]initWithString:placeholder attributes:attr];
  • 關于字典轉模型。這里先以很簡單的單層數(shù)據(jù)為例:

JSON 數(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的實現(xiàn)文件:

@implementation TestModel
- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}
@end

最后是ViewController控制器中的調用代碼:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 加載本地 json 數(shù)據(jù),正常應是請求服務器
    [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. 轉換,只要屬性定義好,直接調用初始化方法即可
    TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
    // 4. 打印驗證
    NSLog(@"name - %@, detail - %@, postscript - %@", model.name, model.detail, model.postscript);
}
@end

打印結果驗證:

打印結果

通過上邊這個十分簡單的例子,我們就可以看到 KVC 在數(shù)據(jù)轉模型時的便捷了。
不過這么簡單的數(shù)據(jù)實際開發(fā)中很難遇到,這里這里只是演示一下,如果有多層嵌套的話,就需要在模型類中重寫setValue: forKey方法,然后通過判斷key,對嵌套的字段再進行轉換。

嵌套的 JSON 數(shù)據(jù)

最外層模型TestModel的頭文件:

@interface TestModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *detail;
@property (copy, nonatomic) NSString *postscript;
// 多了一個嵌套的模型屬性
@property (strong, nonatomic) NestedTestModel *nestedModel;

- (instancetype)initWithDict:(NSDictionary *)dict;
@end

最外層模型TestModel的實現(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 {
    // 單獨處理嵌套的 key
    if ([key isEqualToString:@"nested"]) {
        self.nestedModel = [[NestedTestModel alloc]initWithDict:value];
    } else {
        // 別忘了調用父類的方法,否則其他不需要單獨處理的屬性就不解析了
        [super setValue:value forKey:key];
    }
}
@end

控制器的代碼:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 加載本地 json 數(shù)據(jù),正常應是請求服務器
    [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. 轉換,只要屬性定義好,直接調用初始化方法即可
    TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
    // 4. 打印驗證
    NSLog(@"name - %@, detail - %@, postscript - %@, nestedName - %@, nestedDetail - %@, nestedPostscript - %@, ", model.name, model.detail, model.postscript, model.nestedModel.nestedName, model.nestedModel.nestedDetail, model.nestedModel.nestedPostscript);
}
@end
打印結果

這里嵌套內部的模型NestedTestModel基本跟單層 JSON 中的模型的代碼一致,也就是打印驗證那一點,也就不再貼代碼了。文章最底有git地址。

其實,例子中的數(shù)據(jù)解析跟實際的復雜度也差好多,這時候我們往往會使用第三方框架,簡單方便。這里推薦一個比較小眾的第三方框架:NSObject-ObjectMap,可以看一下它的源文件,寫的很規(guī)整,就一個分類,可以實現(xiàn) json/xml 到對象的轉換,當然集合類的屬性也可以自動轉換(需要多寫一點代碼),里邊有好多方法用都牽扯到了 Runtime 和 OC 中的反射機制。

4. KVC 這么牛掰,系統(tǒng)是怎么實現(xiàn)的呢?

當我們調用setValue: forKey:testKey方法時,系統(tǒng)會通過以下查找順序進行賦值:

setValue:forKey:順序官方文檔

由上圖可以看出,在使用 KVC 賦值時的查找順序依次為:
setTestKey方法 --> _testKey --> _isTestKey --> testKey --> isTestKey。
后邊尋找實例變量的過程,有一個大的前提:對象的accessInstanceVariablesDirectly方法返回YES,當然默認情況下這個方法就是返回YES的,所以并不需要我們擔心。如果都找不到,那就調用 setValue:forUndefinedKey直接拋出異常。有時候如果我們不想讓程序因為這個原因拋出異常,可以在類的實現(xiàn)文件中重寫該方法即可。

代碼Git地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容