【iOS】--KVC与KVO

键值编码(KVC)与键值监听(KVO)

KVC(Key Value Coding)允许以字符串的形式间接操作对象的属性。

简单的KVC

最基本的KVC由NSKeyValueCoding协议提供支持,最基本的操作属性的两个方法如下

  • setVaule:属性值 forKey:为制定属性设置值
  • valueForKey:属性名:获取指定属性的值

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FKUser : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *pass;
@property (nonatomic, copy) NSDate *birth;
@end

NS_ASSUME_NONNULL_END


//

#import <Foundation/Foundation.h>
#import "FKUser.h"
int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        // insert code here...
        NSLog(@"Hello, World!");
        FKUser *user = [[FKUser alloc] init];
        [user setValue:@"花城" forKey:@"name"];
        [user setValue:@"1155" forKey:@"pass"];
        [user setValue:[[NSDate alloc] init] forKey:@"birth"];
        NSLog(@"user的name为%@", [user valueForKey:@"name"]);
        NSLog(@"user的pass为%@", [user valueForKey:@"pass"]);
        NSLog(@"user的birth为%@", [user valueForKey:@"birth"]);
        
    }
    return 0;
}

请添加图片描述
上面的代码先创建一个user对象,在对对象的属性通过KVC方式设置属性值,最后通过KVC方式获取指定属性的值
在KVC编程方式中,无论调用setValue:forKey:方法还是调用valueForKey:方法,都是通过NSString对象来指定被操作属性的,其中forKey标签用于传入属性名

setValue:属性值forKey@“name”代码

  • 程序优先考虑调用“setName:属性值”;代码通过setter方法完成设置
  • 如果该类没有setName:方法,那么KVC机制回搜索该类中名为_name的成员变量,无论该成员变量是在类接口部分定义还是在类实现部分定义,也无论用哪个访问权限控制符修饰,这条KVC代码底层实际上就是对_name成员变量赋值
  • 如果该类即没有setName:方法,也没有定义_name成员变量,那么KVC机制回搜索该类中名为name的成员变量,无论该成员变量是在类接口部分定义还是在类实现部分定义,也无论用哪个访问控制符修饰,这条KVC代码底层实际上就是对name成员变量赋值。
  • 如果上边三步都没有找到,那么系统会执行该对象的setValue:forUndefinrdKey:方法
    默认的valueForUndefinedKey:方法实现就是引发一个异常,这个异常将会导致程序因为异常而结束。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FKDog : NSObject {
    
    
    @package
    NSString *name;
    NSString *_name;
}

@end

NS_ASSUME_NONNULL_END
//

#import "FKDog.h"

@implementation FKDog {
    
    
    int age;
}
@end




        FKDog *dog = [[FKDog alloc] init];
        [dog setValue:@"戚容" forKey:@"name"];
        NSLog(@"dog->name = %@", dog->name);
        NSLog(@"dog->_name = %@", dog->_name);
        [dog setValue:[NSNumber numberWithInt:800] forKey:@"age"];
        NSLog(@"dog的age%@", [dog valueForKey:@"age"]);

请添加图片描述
我们先对dog的name属性进行赋值,但是由于FKDog类并不存在setName:方法,因此我们对dog的成员变量_name进行赋值。我们输出dog->name时,可以看到输出“戚容”。所以我们输出dog->name时为空,而输出dog->_name时不为空。
最后我们通过KVC对dog的age属性赋值并访问

处理不存在的key

当我们使用KVC方式操作属性时,这些属性可能并不存在,即不存在对应的setter,getter方法,也不存在对应的成员变量,KVC会自动调用setValue:forUndefinedKey:和valueForUndefinedKey:方法,但是系统默认实现的这两个方法仅仅是引发异常,并没有进行任何特别处理

//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FKApple : NSObject

@end

NS_ASSUME_NONNULL_END



//

#import "FKApple.h"

@implementation FKApple

@end



FKApple *apple = [[FKApple alloc] init];
        [apple setValue:@"monster" forKey:@"name"];
        [apple valueForKey:@"name"];
        

请添加图片描述
提示说程序尝试设定的name对象不存在,因此引发异常。

这句提示就是setValue:forUndefinedKey方法的默认实现,这个实现引发一个NSUnknownKeyExpection异常,并结束程序,这个默认方法的实现可能并不适合实际,可能我们不想让他结束。这时候就要重写这个方法。

//

#import "FKApple.h"

@implementation FKApple
- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key{
    
    
    NSLog(@"这个key不存在,你是不是敲错了?");
    NSLog(@"你尝试设定的value为:%@", value);
}
@end

请添加图片描述
显而易见,我们通过KVC设置对象的属性时没有发生任何异常,那为什么还是崩了呢?
因为还有这一句代码

[apple valueForKey:@“name”];

我们尝试通过KVC访问对象的name key,因为我们不存在name方法,也不存在name,_name成员变量,因此KVC会调用valueForUndefinedKey:方法,这时我们重写这个方法。
请添加图片描述


#import "FKApple.h"

@implementation FKApple
- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key{
    
    
    NSLog(@"这个key不存在,你是不是敲错了?");
    NSLog(@"你尝试设定的value为:%@", value);
}
- (id)valueForUndefinedKey:(NSString *)key {
    
    
    NSLog(@"这个key不存在,你是不是敲错了?");
    return key;
}
@end

可以发现重写之后就没有异常了。
当KVC操作不存在的key时,KVC机制总是调用重写过的方法进行处理,通过这种处理机制,可以非常方便的定制自己的处理行为。

处理nil值

当通过KVC设置对象的属性时,如果属性是基本类型(int, float,doule),并且我们给他一个对应的属性值,那么程序可以正确的进行设置,但是如果我们为基本类型的属性设置一个nil,会发生什么事情呢,KVC会把这个东西当作0还是1?

//  Created by 王璐 on 2023/5/13.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FKItem : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int price;
@end


NS_ASSUME_NONNULL_END



FKItem *item = [[FKItem alloc] init];
        [item setValue:nil forKey:@"name"];
        [item setValue:nil forKey:@"price"];
        NSLog(@"item的name为%@", [item valueForKey:@"name"]);
        NSLog(@"item的price为%@", [item valueForKey:@"price"]);
        
        

我们设置了两个属性,一个是NSString类型,它可以接受nil,另一个是int类型,不能接受nil。
请添加图片描述
提示我们引发了一个NSInvidalArguementExpection异常。这是因为int类型的值不能接受nil造成的
这段提示信息是由程序中的setNilValueForKey:方法所产生的。也就是说,当程序尝试为某个属性值设置nil值时,如果该属性并不接受nil值,那么程序会自动执行该属性的setNilValueForKey:方法。我们重写一下这个方法。

//

#import "FKItem.h"

@implementation FKItem
- (void)setNilValueForKey:(NSString *)key{
    
    
    if ([key isEqualToString:@"price"]) {
    
    
        _price = 0;
    } else {
    
    
        [super setNilValueForKey:key];
    }
}
@end

请添加图片描述
如果我们尝试将key为price的属性设置为nil,那么程序将会直接将_price成员变量设置为0,说明FKItem的price属性会把nil当成0处理,设置其他属性则不做处理。

key路径

KVC除了可操作对象的属性外,还可以操作对象的复合属性。所谓复合属性,KVC将其称为key路径。比如KFOrder对象内包含一个FKItem的类型的属性item,FKitem又包含了name属性和price属性,那么KVC就可以通过item.name,item.price这种key路径来操作FKOrder对象中的item属性和price属性
在KVC中操作key路径的方法如下。

  • setValue:forKeyPath:根据key路径设置属性值
  • valueForKeyPath:根据key路径获取属性值

请添加图片描述


#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN

@interface FKOrder : NSObject
@property (nonatomic, strong) FKItem *item;
@property (nonatomic, assign) int amount;
- (int)totalPrice;
@end

NS_ASSUME_NONNULL_END



FKOrder *order = [[FKOrder alloc] init];
        [order setValue:@"5000" forKey:@"amount"];
        [order setValue:[[FKItem alloc] init] forKey:@"item"];
        [order setValue:@"书包" forKeyPath:@"item.name"];
        [order setValue:[NSNumber numberWithInt:5] forKeyPath:@"item.price"];
        NSLog(@"订单包含%@个%@, 总价为%@",[order valueForKey:@"amount"], [order valueForKeyPath:@"item.name"], [order valueForKey:@"totalPrice"]);
        

setValue:forKey:,valueForKey:设置复合属性并获取复合属性的值,这样就可以直接操作FKOreder对象中的item属性的name属性了,也可以直接操作FKOreder对象中item属性的price属性
OC的集合同样支持使用KVC进行整体操作
为什么要用KVC来操作呢?使用setter,getter不行吗?是不是这样效率比较高呢?实际上,通过KVC操作的性能比setter,getter更差,使用KVC的优势在于更灵活,更适合提炼一些通用性质的代码。因为KVC的方式允许通过字符串的方式来操作对象的属性,这个字符串既可以是常量,也可以是变量。因此具有极高的灵活性。

KVO(键值坚听)

在iOS应用开发过程中,iOS应用通常会把应用程序组件分开成为数据模型组件和视图组件,其中数据模型组件负责维护应用程序的状态数据,视图组件负责显示数据模型组件内部的状态数据。

我们的需求是:在数据模型组件的状态数据发生改变时,视图组件能动态的更新自己,及时显示数据模型组件更新后的数据。
在这里插入图片描述
请添加图片描述

//

#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN

@interface FKItemView : NSObject
@property (nonatomic, weak) FKItem *item;
- (void)showItemInfo;
@end

NS_ASSUME_NONNULL_END

为了能让FKItemView监听得到FKItem组件的状态改变,我们需要为FKItem组件添加一个监控属性改变的监听器。并重写监听器的observerValueForKeyPath:change:context:方法

//

#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN

@interface FKItemView : NSObject
@property (nonatomic, weak) FKItem *item;
- (void)showItemInfo;
@end

NS_ASSUME_NONNULL_END



//

#import "FKItemView.h"

@implementation FKItemView
- (void)showItemInfo {
    
    
    NSLog(@"物品名称为%@, 物品价格为%d", self.item.name, self.item.price);
}
- (void)setItem:(FKItem *)item {
    
    
    self->_item = item;
    [self.item addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    [self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];
    
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    
    NSLog(@"bserveValueForKeyPath");
    NSLog(@"keyPath:%@", keyPath);
    NSLog(@"object:%@", object);
    NSLog(@"change objectForKey:%@", [change objectForKey:@"new"]);
    NSLog(@"change:%@", context);
}
- (void)dealloc {
    
    
    [self.item removeObserver:self forKeyPath:@"name"];
    [self.item removeObserver:self forKeyPath:@"price"];
}
@end



FKItem *item = [[FKItem alloc] init];
        item.name = @"花城";
        item.price = 800;
        FKItemView *view = [[FKItemView alloc] init];
        view.item = item;
        [view showItemInfo];
        item.name = @"huacheng";
        item.price = 10000;
        

请添加图片描述
我们可以看到监听器监听方法的输出,可以看出监听器完全可以得到我们修改的属性,接下来我们把这些修改的属性加到UI上就好了。

猜你喜欢

转载自blog.csdn.net/weixin_61196797/article/details/130647341