KVC

KVC definition

KVC (Key-Value Coding) is iOS development that allows developers to directly access or assign values to the attributes of an object through the Key name. Without calling explicit access methods. This allows the properties of the object to be accessed and modified dynamically at run time. Not at compile time, which is one of the dark arts of iOS development. Many advanced iOS development techniques are based on KVC.

In classes that implement accessor methods, there is little difference between using dot syntax and KVC to access objects, and the two can be used in any mix. But in classes that do not have access to methods, dot syntax is not available, and KVC has an advantage.

KVC is defined as an extension to NSObject, and there’s an explicit NSKeyValueCoding class name in Objective-C, so you can use KVC for all types that inherit from NSObject (some pure Swift classes and constructs don’t support KVC, Since there is no NSObject inheritance), here are the four most important methods in KVC:

- (nullable id)valueForKey:(NSString *)key; // Select Key from Key - (void)setValue:(nullable id)value forKey:(NSString *)key; Value (nullable id)valueForKeyPath:(NSString *)keyPath; // Value by KeyPath - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // Set the value by KeyPathCopy the code

Some other methods in the NSKeyValueCoding category:

+ (BOOL)accessInstanceVariablesDirectly; // The default return is YES, which means that if the Set<Key> method is not found, members will be searched in _key, _iskey, Key, iskey order. - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValueforKey:(NSString *)inKey error:(out NSError **)outError; //KVC provides an API to verify that property values are correct. It can be used to checksetIs the correct value for, makes a replacement value for an incorrect value, or refuses to set a new value and returns the reason for the error. - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; If the property is an NSMutableArray, then you can use this method to return the property. - (nullable id)valueForUndefinedKey:(NSString *)key; // This method is called if the Key does not exist and no KVC can find any fields or properties related to the Key. The default is to throw an exception. - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; // The same as the previous method, but this method sets the value. - (void)setNilValueForKey:(NSString *)key; // If you pass nil to Value during SetValue, Will call this method - (NSDictionary < nsstrings *, id > *) dictionaryWithValuesForKeys: (NSArray keys < > nsstrings * *); // Enter a set of keys, return the Value corresponding to the set of keys, and return the dictionary, which is used to transfer the Model to the dictionary.Copy the code

And apple has a special implementation for some container classes like NSArray or NSSet, KVC.

The corresponding method of ordered set is as follows:

-countof <Key>// Must be implemented, corresponding to the basic method count:2 -objectIn<Key>AtIndex: -<key>AtIndexes:// One of these two must be implemented, which corresponds to the NSArray methods objectAtIndex: and objectsAtIndexes: -get<Key>:range:// Not required, but can improve performance. This corresponds to the NSArray method getObjects:range: -insertobject:in<Key>AtIndex: -insert<Key>:atIndexes:// Both must implement one, similar to the methods insertObject: AtIndex: and insertObjects:atIndexes: -removeObjectFrom<Key>AtIndex: -removeObjectfrom <Key>AtIndex: -remove<Key>AtIndexes: And removeObjectsAtIndexes: - replaceObjectIn < Key > AtIndex: withObject: -replace<Key>AtIndexes:with<Key>:// Optional, if there are performance issues with this operation, consider implementing itCopy the code

The corresponding methods of unordered sets are as follows:

-countof <Key>// must implement, corresponding to the basic NSArray method count: -objectin <Key>AtIndex: -<key>AtIndexes:// One of these two must be implemented, which corresponds to the NSArray methods objectAtIndex: and objectsAtIndexes: -get<Key>:range:// Not required, but can improve performance. This corresponds to the NSArray method getObjects:range: -insertobject:in<Key>AtIndex: -insert<Key>:atIndexes:// Both must implement one, similar to the methods insertObject: AtIndex: and insertObjects:atIndexes: -removeObjectFrom<Key>AtIndex: -removeObjectfrom <Key>AtIndex: -remove<Key>AtIndexes: And removeObjectsAtIndexes: - replaceObjectIn < Key > AtIndex: withObject: -replace<Key>AtIndexes:with<Key>:// Both of these are optional and should be considered if there are performance issues with this type of operationCopy the code

The technical concepts and usage of KVC are explained from the following aspects:

  • KVC set value
  • KVC values
  • Use KVC keyPath
  • KVC handles exceptions
  • KVC handles numeric and struct type attributes
  • KVC Key-value Validation
  • KVC deals with collections
  • KVC handles dictionaries

Technical concepts related to KVC

KVC set value

If KVC wants to set values, then we need the corresponding keys in the object, and in what order KVC internally looks for the keys. When calling the setValue: attribute value forKey: @ “name” code, the underlying execution mechanism is as follows:

  • The program calls the set: property value method first, and the code uses the setter method to complete the setting. Note, here refers to the member variable name, the first letter case should conform to KVC naming rules, the same below

  • If setName is not found: Methods, mechanism of KVC will check + (BOOL) have accessInstanceVariablesDirectly method returns YES, by default, this method returns YES if you rewrite the method make its return NO, so in this step KVC will perform setValue: ForUndefinedKey: method, but most developers don’t do this. So the KVC mechanism will search for a member variable named _ in the class, no matter whether the variable is defined at the class interface or at the class implementation, and no matter what access modifier is used, if there is only a variable named _, KVC can assign a value to that member variable.

  • If the class has neither a set: method nor a _ member variable, the KVC mechanism searches for the _is member variable.

  • As above, if the class has no set: method and no _ and _is member variables, the KVC mechanism will continue to search for the and is member variables. And then assign them values.

  • If none of the methods or member variables listed above exist, the setValue: forUndefinedKey: method on the object will be executed. By default, an exception will be thrown.

In short, if no Set

method is found, members will be searched and assigned in the order _key, _iskey, Key, iskey.

If developers want this class to disable the KVC, then rewrite the + (BOOL) accessInstanceVariablesDirectly method to return NO, so if the KVC had not found the set: the property name, can directly use setValue: ForUndefinedKey: method.

Here’s an example:

#import <Foundation/Foundation.h>@interface Test: NSObject { NSString *_name; } @end @implementation Test @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // assign name [obj] via KVCsetValue:@"xiaoming" forKey:@"name"]; // Print NSLog(@) with KVC value name"Obj's name is %@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
Copy the code

2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] Obj’s name is xiaoming

– (void)setValue:(nullable id)value forKey:(NSString *)key; And – (nullable id)valueForKey:(NSString *)key; The name value of the obj object is successfully set and fetched.

Look at the setting accessInstanceVariablesDirectly to NO effect:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_name;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key", key); } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // assign name [obj] via KVCsetValue:@"xiaoming" forKey:@"name"]; // Print NSLog(@) with KVC value name"Obj's name is %@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
Copy the code

Print result: 2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] Name 2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] Name 2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj = (null)

When can see accessInstanceVariablesDirectly to NO KVC can only query setter and getter this layer, the following relevant variables execution will stop looking for the key, an error directly.

Set accessInstanceVariablesDirectly to YES, then modify _name _isName, see if successful execution.

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_isName;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key", key); } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // assign name [obj] via KVCsetValue:@"xiaoming" forKey:@"name"]; // Print NSLog(@) with KVC value name"Obj's name is %@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
Copy the code

2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] Obj’s name is xiaoming

From printing can see set accessInstanceVariablesDirectly to YES, KVC will continue in accordance with the sequential search, and successfully set value and the value.

KVC values

When calling the code for valueForKey: @ “name”, KVC searches for keys differently than setValue: attribute valueForKey: @ “name”, as follows:

  • First of all, we look for the getter method in the order of get, is, and if we find it, we call it directly. If it’s a BOOL or an Int equivalent, it wraps it up as an NSNumber object.

  • If the getter above is not found, KVC will look for methods in countOf,objectInAtIndex, or AtIndexes format. If the countOf method and one of the other two methods are found, then it returns a collection of proxies (which is NSKeyValueArray, a subclass of NSArray) that respond to all the methods of NSArray, and calls the methods of this collection of proxies, Or send a method that belongs to an NSArray to this proxy collection, and it’s called as a combination of countOf,objectInAtIndex, or AtIndexes. There is also an optional get:range: method. So if you want to redefine some of the functionality of KVC, you can add these methods, but be careful that your method names conform to KVC’s standard named methods, including method signatures.

  • If none of the above methods are found, then methods in countOf, enumeratorOf, and memberOf formats are also looked for. If all three methods are found, then it returns a collection of proxies that respond to all the methods of the NSSet. As above, sending the NSSet message to the collection of proxies is called as a combination of countOf, enumeratorOf, and memberOf.

  • If you haven’t found, check class method + (BOOL) accessInstanceVariablesDirectly, if return YES (default behavior), then the set value and the previous, will follow the _, _is, member variable name is the order of the search, here are not recommended to do so, Because such direct access to instance variables breaks encapsulation and makes the code more vulnerable. If you rewrite the class method + (BOOL) accessInstanceVariablesDirectly return NO, then can call directly valueForUndefinedKey: method, default is an exception.

Add the getAge method to the Test class, for example:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)getAge {
    return10; } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // Print NSLog(@) with the KVC value age"Obj's age is %@.", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

[35324:6188613] Obj’s age is 10

You can see that [obj valueForKey:@”age”], finds the getAge method, and gets the value.

Let’s change getAge to age. Here’s an example:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)age {
    return10; } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // Print NSLog(@) with the KVC value age"Obj's age is %@.", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

[35337:6195086] The age of OBj is 10

You can see that [obj valueForKey:@”age”] finds the age method and gets the value.

Let’s change getAge to isAge. Here’s an example:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)isAge {
    return10; } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Test *obj = [[Test alloc] init]; // Print NSLog(@) with the KVC value age"Obj's age is %@.", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

[35345:6201242] Obj’s age is 10

You can see that [obj valueForKey:@”age”] finds the isAge method and gets the value.

The above code illustrates the mechanism by which KVC searches for key when calling valueforKey:@”age”.

Use KVC keyPath

In the development process, a class member variable may be a custom class or other complex data type, you can first use KVC to get the property, and then use KVC to get the property of the custom class, but this is more tedious, KVC provides a solution, that is the keyPath. As the name implies, it follows the path to find the key.

- (nullable id)valueForKeyPath:(NSString *)keyPath; // Value by KeyPath - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // Set the value by KeyPathCopy the code

The code is as follows:

#import <Foundation/Foundation.h>@interface Test1: NSObject { NSString *_name; } @end @implementation Test1 @end @interface Test: NSObject { Test1 *_test1; } @end @implementation Test @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... //Test generates object Test *test= [[Test alloc] init]; //Test1 generates object Test1 *test1 = [[Test1 alloc] init]; // Set the value via KVCtestthe"test1"
        [test setValue:test1 forKey:@"test1"]; // Set the value via KVCtesttheThe name "test1"
        [test setValue:@"xiaoming" forKeyPath:@"test1.name"]; // Print NSLog(@) with the KVC value age"Test1 name\" is %@"[test valueForKeyPath:@"test1.name"]);
        
    }
    return 0;
}
Copy the code

2018-05-05 16:19:02.613394+0800 KVCKVO[35436:6239788] test “name” is xiaoming

The print shows that we successfully set test1 to the keyPath value. KVC is the search mechanism for keyPath and the first step is to separate the key and use the decimal point. To split keys, and then search in the same order as normal keys.

KVC handles exceptions

The most common exceptions in KVC are accidentally using the wrong key, or accidentally passing a nil value in a set value, and KVC has special methods for handling these exceptions.

KVC handles nil exceptions

In general, KVC does not allow you to pass a nil value to a non-object when calling setValue: property value forKey (or keyPath). That’s easy, because value types can’t be nil. If you accidentally pass it in, KVC will call the setNilValueForKey: method. This method throws an exception by default, so it is generally best to override this method.

Code implementation is as follows:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger age;
}

@end

@implementation Test

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"You can't set %@ to nil.", key); } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... //Test generates object Test *test= [[Test alloc] init]; // Set the value via KVCtestThe age oftest setValue:nil forKey:@"age"]; // Print NSLog(@) with the KVC value age"The age of test is %@"[test valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

Print result: 2018-05-05 16:24:30.302134+0800 KVCKVO[35470:6258307] Can’t set age to nil The age of test is 0

KVC handles the UndefinedKey exception

In general, KVC does not allow you to operate on nonexistent keys when calling setValue: property value forKey (or keyPath). Otherwise, we’re going to say, “forUndefinedKey crashed,” so override forUndefinedKey to avoid the crash.

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"An exception has occurred where %@ does not exist for this key", key); } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... //Test generates object Test *test= [[Test alloc] init]; // Set the value via KVCtestThe age oftest setValue:@10 forKey:@"age"]; // Print NSLog(@) with the KVC value age"The age of test is %@"[test valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

Print result: 2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] Age 2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] [3557:6277523] test age = (null)

KVC handles numeric and struct type attributes

Not every method returns an object, but valueForKey: always returns an ID object, and if the original variable type was a value type or a structure, the return value is encapsulated as an NSNumber or NSValue object. These two classes handle everything from numbers and booleans to Pointers and structures. Then the user needs to manually convert to the original type. Although valueForKey: automatically encapsulates the value type as an object, setValue: forKey: does not. You have to manually convert the value type to NSNumber or NSValue in order to pass it. Since what is passed in and out is an ID type, it is up to the developer to make sure that the type is correct. At runtime objective-C checks the type when sending the message and throws an exception if it is incorrect.

For example, the Person class has an AGE attribute of type NSInteger, as follows:

//  Person.m
#import "Person.h"
 
@interface Person ()
 
@property (nonatomic,assign) NSInteger age;
 
@end
 
 
@implementation Person
 
@end
Copy the code

Modify the value

We use the KVC technique to set the value of the age attribute as follows:

[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
Copy the code

We assign an NSNumber object to age, and KVC will automatically convert the NSNumber object to an NSInteger object, and then call the corresponding accessor method to set the value of age.

Get the value

Again, get the age attribute as follows:

[person valueForKey:@"age"];
Copy the code

In this case, we return the value of age in the form of NSNumber.

//  ViewController.m
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc]init];
    [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
    NSLog(@"age=%@",[person valueForKey:@"age"]);
    
}


@end
Copy the code

2017-01-16 16:31:55.709 Test[28586:2294177] age=5

The thing to notice is that we can’t assign a value directly through KVC, we need to pass in NSNumber and NSValue, so what types of data do we want to wrap around NSNumber and what types of data do we want to wrap around NSValue? Look at the parameter types of these methods:

The types of data you can use NSNumber are:

+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value; + (NSNumber*)numberWithDouble:(double)value; + (NSNumber*)numberWithBool:(BOOL)value; + (NSNumber *) numberWithInteger: (NSInteger) valueNS_AVAILABLE (10 _5, 2 _0); + (NSNumber *) numberWithUnsignedInteger: NSUInteger valueNS_AVAILABLE (10 _5, 2 _0);Copy the code

It’s just the usual numeric data.

The types of data you can use NSValue are:

+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);
Copy the code

NSValue is primarily used to deal with structured data, which itself provides support for the above centralized structure. Any structure can be converted to an NSValue object, including any other custom structure.

KVC Key-value Validation

KVC provides a way to verify that the Value corresponding to the Key is available:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
Copy the code

The default implementation of this method is to call a method with the following format:

- (BOOL)validate<Key>:error:
Copy the code

Such as:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger _age;
}

@end

@implementation Test

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    
    returnYES; } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... //Test generates object Test *test= [[Test alloc] init]; // Set the value via KVCtestAge NSNumber *age = @10; NSError* error; NSString *key = @"age";
        BOOL isValid = [test validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"Key-value matching");
            [test setValue:age forKey:key];
        }
        else {
            NSLog(@"Key-value mismatch"); } // print NSLog(@) with the KVC value age"The age of test is %@"[test valueForKey:@"age"]);
        
    }
    return 0;
}
Copy the code

Print result: 2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] 2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] The age of test is 0

This gives us a chance to correct the error. It should be noted that KVC does not automatically call the key-value validation method, which means we need to do it manually if we want it. But some techniques, such as CoreData, are called automatically.

KVC deals with collections

KVC also provides complex functions, including the following:

Simple set operator

There are five simple set operators: @avg, @count, @max, @min, and @sum. let’s see what they mean.

#import <Foundation/Foundation.h>

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

@implementation Book
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 10;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 40;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f",sum.floatValue);
        NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f",avg.floatValue);
        NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f",count.floatValue);
        NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f",min.floatValue);
        NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f",max.floatValue);
        
    }
    return 0;
}
Copy the code

Print result: 2018-05-05 17:04:50.674243+0800 KVCKVO[35785:6351239] SUM :100.000000 2018-05-05 17:04:50.675007+0800 KVCKVO[35785:6351239] avg:25.000000 2018-05-05 17:04:50.675081+0800 KVCKVO[35785:6351239] count:4.000000 2018-05-05 Min :10.000000 2018-05-05 17:04:50.675204+0800 KVCKVO[35785:6351239] Max: 40.000000

Object operator

Slightly more complex than the collection operator, it can return the specified contents as an array. There are two types:

  • @distinctUnionOfObjects
  • @unionOfObjects

They both return an NSArray, but the difference is that the former returns a unique element, which is the result of the deduplicated element; The latter returns a complete set of elements.

Such as:

#import <Foundation/Foundation.h>

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

@implementation Book
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 40;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 10;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        
        NSLog(@"distinctUnionOfObjects");
        NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        for (NSNumber *price in arrDistinct) {
            NSLog(@"%f",price.floatValue);
        }
        NSLog(@"unionOfObjects");
        NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        for (NSNumber *price in arrUnion) {
            NSLog(@"%f",price.floatValue); }}return 0;
}
Copy the code

Print result: 2018-05-05 17:06:21.832401+0800 KVCKVO[35798:6358293] distinctUnionOfObjects 2018-05-05 17:06:21.833079+0800 KVCKVO[35798:6358293] 10.000000 2018-05-05 17:06:21.833112+0800 KVCKVO[35798:6358293] 20.000000 2018-05-05 [35798:6358293] 30.000000 2018-05-05 17:06:21.833146+0800 KVCKVO[35798:6358293] 40.000000 2018-05-05 17:06:21.833165+0800 KVCKVO[35798:6358293] unionOfObjects 2018-05-05 17:06:21.833297+0800 KVCKVO[35798:6358293] 40.000000 2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000 2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000 2018-05-05 [35798:6358293] 30.000000 2018-05-05 17:06:21.833388+0800 KVCKVO[35798:6358293] 10.000000

KVC handles dictionaries

When using KVC on NSDictionary objects, valueForKey: behaves the same as objectForKey:. So it is convenient to use valueForKeyPath: for accessing a multi-layer nested dictionary.

KVC also has two methods for NSDictionary:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
Copy the code

DictionaryWithValuesForKeys: refers to input a set of key, returns the set of key corresponding to the attribute, then form a dictionary. SetValuesForKeysWithDictionary is used to modify the Model of the corresponding key attributes. It’s a little more intuitive to just use the code:

#import <Foundation/Foundation.h>@interface Address : NSObject @end @interface Address() @property (nonatomic, copy)NSString* country; @property (nonatomic, copy)NSString* province; @property (nonatomic, copy)NSString* city; @property (nonatomic, copy)NSString* district; @end @implementation Address @end int main(int argc, Const char * argv[]) {@autoreleasepool {Address* add = [Address new]; add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country"The @"province"The @"city"The @"district"]; NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; NSLog(@) = NSLog(@)"% @",dict); NSDictionary* modifyDict = @{@"country": @"USA"The @"province": @"california"The @"city": @"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict]; // modify the Model property NSLog(@) with key Value"country:%@ province:%@ city:%@",add.country,add.province,add.city);
        
        
    }
    return 0;
}
Copy the code

2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] {city = “Zhen”; country = China; district = “Nan Shan”; province = “Guang Dong”; } 2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province: California city:Los Angle

The print came out exactly as expected.

KVC use

KVC is an indispensable tool in iOS development. This run-time programming method greatly improves flexibility, simplifies code, and even achieves many unimaginable functions. KVC is also the basis of many dark arts of iOS development. The following is a list of usage scenarios for KVC in iOS development.

Value and set values dynamically

Dynamic values and Settings using KVC are the most basic uses.

Use KVC to access and modify private variables

For private properties in a class, objective-C is not directly accessible, but KVC is.

Model and dictionary transformations

This is another example of the power of KVC. The runtime combination of KVC and Objc makes it easy to convert models and dictionaries.

Modify the internal properties of some controls

This is also an essential tip in iOS development. It is well known that many UI controls are composed of many internal UI controls, but Apple does not provide an API to access these controls, so we can not normally access and modify the style of the controls. KVC can solve this problem in most cases. The most commonly used is the brief text in a personalized UITextField.

Operating collection

Apple has some special implementations of KVC’s valueForKey: methods, and container classes like NSArray and NSSet implement these methods. So you can easily manipulate sets with KVC.

High – order message delivery with KVC

When KVC is used on a container class, valueForKey: is passed to each object in the container, not to the container itself. The results are added to the returned container so that developers can easily manipulate one set to return another.

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSArray* arrStr = @[@"english"The @"franch"The @"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString* str  in arrCapStr) {
            NSLog(@"% @",str);
        }
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue); }}return 0;
}
Copy the code

Print result: 2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] Franch 2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese 2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7 2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 8 2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 7

The method capitalizedString is passed to each item in an NSArray, so that each member of an NSArray executes capitalizedString and returns a new NSArray with the result. As you can see from the print, all strings have been successfully converted to uppercase. You can also use the valueForKeyPath: method if you want to execute multiple methods. It calls capitalizedString for each member, then length, and because lenth returns a number, the result is stored in a new array as an NSNumber.

Realize the KVO

KVO is based on KVC. The concept and implementation of KVO are described below.

KVO

KVO definition

KVO, or key-value Observing, translates to key-value Observing. It is a derivative of the observer pattern. The basic idea is to add an observation to a property of the target object, and automatically notify the observer when the property changes by triggering the KVO interface method implemented by the observer object.

What is the observer mode? A target object manages all the observer objects that depend on it, and proactively notifying the observer objects when its own state changes. This active notification is usually implemented by calling the interface methods provided by each observer object. The observer pattern perfectly decouples the target object from the observer object.

Basically, KVO can listen for a change in value by listening for a change in key, and it can be used to listen for a change in state between objects. KVO is defined as an extension to NSObject. Objective-c has an explicit NSKeyValueObserving class name, so you can use KVO for any type that inherits from NSObject. (Some pure Swift classes and structures do not support KVC. Because there is no NSObject inheritance).

Use KVO

Registration and de-registration

If we have contains the class for the key value observing attributes, so can be used in the class of objects (observed object) is invoked on the category method called NSKeyValueObserverRegistration observer and the observed object to registration of the registration:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
Copy the code
Observer: An observer, that is, a subscriber to a KVO notification. Subscription must realize observeValueForKeyPath: ofObject: change: context: method keyPath: description will observe the properties of the relative to the observer. Options: some properties of KVO configuration; There are four choices. Context: This is passed to the subscribed function to distinguish messages, so it should be different.Copy the code

Options Specifies what to include

NSKeyValueObservingOptionNew: After change dictionary involves changing the value of the NSKeyValueObservingOptionOld: change the dictionary include changing the value of the former NSKeyValueObservingOptionInitial: after registration immediately trigger the KVO notifications Whether to also want to notice before NSKeyValueObservingOptionPrior: value change (the key to determine whether the change notification is twice before change)Copy the code

The definition of these two methods in Foundation/NSKeyValueObserving. J h, NSObject, NSArray, NSSet implements the above method, so we can observe not only ordinary objects, also can observe the array or a combination of class object. In the header file, we can also see NSObject also implements NSKeyValueObserverNotification category method (more similar methods, please check the header file NSKeyValueObserving. H) :

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
Copy the code

These two methods are used to manually implement key-value observation. Be sure not to unregister when not in use, as this may cause memory leaks.

Process change notice

The callback is made in this function whenever the listening keyPath changes.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
Copy the code

Here, change the dictionary keeps a change information, what are the specific information depends on the NSKeyValueObservingOptions when registering.

Manual KVO(disable KVO)

The KVO implementation automatically implements two functions on the registered keyPath, and then calls them automatically in the setter.

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
Copy the code

There may be times when we want to implement KVO manually, or __ when we implement a library that does not want to be KVO__. In this case, you need to turn off the automatic generation of KVO notifications and call them manually. The advantage of manual notifications is that you have the flexibility to add the criteria you want. Here’s an example:

@interface Target : NSObject { int age; } / /for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if(nil ! = self) { age = 10; }returnself; } / /for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end
Copy the code

First, you need to manually implement the setter method for the property and call the willChangeValueForKey: and didChangeValueForKey methods before and after the setting operation. These methods are used to notify the system that the value of the key property is about to change and has changed. Second, be automaticallyNotifiesObserversForKey implementation class methods, and in the setting of the key does not automatically send a notification (return). Note that other keys that are not implemented manually will be handed over to super. If need __ to disable these KVO__ automaticallyNotifiesObserversForKey directly return NO, realize attribute setter method, don’t make calls willChangeValueForKey: And the didChangeValueForKey method.

Key values observe dependent keys

Sometimes the value of an attribute depends on one or more attributes in another object. If the value of any of these attributes changes, the value of the dependent attribute should also mark the change. Therefore, object introduces dependent keys.

Observation dependent key

Looking at the way relying on key as described earlier, the first in the Observer observeValueForKeyPath: ofObject: change: context: add code to handle change notification:

#import "TargetWrapper.h"

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else{ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}Copy the code
Implement dependent keys

In this case, you’re looking at the Information attribute of the TargetWrapper class, which is dependent on the age and grade attributes of the Target class. To do this, I add the grade attribute to Target:

@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end
Copy the code

Let’s look at how the TragetWrapper’s dependent-key property works.

@class Target;

@interface TargetWrapper : NSObject
{
@private
    Target * _target;
}

@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;

-(id) init:(Target *)aTarget;

@end

#import "TargetWrapper.h"
#import "Target.h"

@implementation TargetWrapper

@synthesize target = _target;

-(id) init:(Target *)aTarget
{
    self = [super init];
    if(nil ! = self) { _target = [aTarget retain]; }return self;
}

-(void) dealloc
{
    self.target = nil;
    [super dealloc];
}

- (NSString *)information
{
    return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}

- (void)setInformation:(NSString *)theInformation
{
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age"The @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
//    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    NSArray * moreKeyPaths = nil;
//    
//    if ([key isEqualToString:@"information"])
//    {
//        moreKeyPaths = [NSArray arrayWithObjects:@"target.age"The @"target.grade", nil]; / / / / / /}if (moreKeyPaths)
//    {
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths]; / / / / / /}return keyPaths;
//}

@end
Copy the code

First, manually implement the setter/getter method for the property Information, where the Target property is used to complete its setter and getter.

Secondly, in order to realize keyPathsForValuesAffectingInformation or keyPathsForValuesAffectingValueForKey: Method to tell the system what other properties the information property depends on. Both methods return a collection of key-paths. Here want to say a few words more, if you choose to implement keyPathsForValuesAffectingValueForKey, must first obtain super returned result set, and then figure out whether the key target key, If yes, append the key-path combination of the dependent attribute to the result set returned by super; otherwise, return the result of super directly.

In this case, the information attribute depends on the age and grade attributes of target, and information observers are notified of any changes to the age/grade attributes of target.

Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];

TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
          forKeyPath:@"information"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:[TargetWrapper class]];

[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
Copy the code

Class: TargetWrapper, Information changed old Information is 0#10 new Information is 0#30 class: TargetWrapper, Information changed old information is 0#30 new information is 1#30

KVO and thread

One thing to note is that the KVO behavior is synchronous and occurs on the same thread as the observed value change. No queue or run-loop processing. Manual or automatic call -didChange… The KVO notification is triggered.

Therefore, we should be careful when attempting to change property values from other threads unless we are sure that all observers are handling KVO notifications in a thread-safe manner. In general, we don’t recommend mixing KVO with multithreading. If we are going to use multiple queues and threads, we should not use KVO between them.

KVO runs synchronously. This feature is very powerful, as long as we are running on a single thread (such as the main queue), KVO guarantees the following two things:

First, if we call a setter method that supports KVO, it looks like this:

The self. The exchangeRate = 2.345;Copy the code

KVO ensures that all observers of exchangeRate are notified before the setter method returns.

Second, if a key was observed with NSKeyValueObservingOptionPrior options, until – observe… The accessor method of exchangeRate returns the same value until called.

KVO implementation

KVO is implemented through ISA-Swizzling. The basic process is that the compiler automatically creates a derived class for the observed object and points the observed object’s ISA to this derived class. If the user registers an observation on a property of the target object, the derived class overrides the method and adds code to notify it. When objective-C sends a message, it uses the ISA pointer to find the class object to which the current object belongs. The class object holds the instance methods of the current object, so when you send a message to this object, you are actually sending a method to a derived object. Because the compiler override the method of the derived class and adds the notification code, a notification is sent to the registered object. Note that the derived class overrides only the attribute methods that register observers.

The official Apple documentation is as follows:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

The KVO implementation relies on Runtime’s powerful dynamic capabilities.

When an object of type ObjectA is added, the system generates an NSKVONotifying_ObjectA class and points the object’s ISA pointer to the new class, which means that the object’s type has changed. This class overrides the following methods compared to ObjectA.

Rewrite the setter

In the setter, calls to the following two methods are added.

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
Copy the code

Then in didChangeValueForKey:, call:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context;
Copy the code

Contains notification of new and old values.

Notification of attribute value changes is implemented. Because KVO works by modifying setter methods, to use KVO you must call the setter. There is no effect if you access the property object directly.

Rewrite the class

When you change the isa point, the return value of class does not change, but the value of ISA does.

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface ObjectA: NSObject

@property (nonatomic) NSInteger age;

@end

@implementation ObjectA
@end

@interface ObjectB: NSObject
@end

@implementation ObjectB

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"% @", change); } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... ObjectA *objA = [[ObjectA alloc] init]; ObjectB *objB = [[ObjectB alloc] init]; // After adding an Observer [objA addObserver:objBforKeyPath:@"age"options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; // Output ObjectA NSLog(@"% @", [objA class]); // Output NSKVONotifying_ObjectA (object_getClass returns isa pointed to) NSLog(@)"% @", object_getClass(objA));

    }
    return 0;
}
Copy the code

Print result: 2018-05-06 22:47:05.538899+0800 KVCKVO[3847:13343992] ObjectA 2018-05-06 22:47:05.538899+0800 KVCKVO[3847:13343992] NSKVONotifying_ObjectA

Override dealloc

The system overrides the dealloc method to free up resources.

Rewrite _isKVOA

This private method is used to indicate that the class is a KVO claimed class.

How do I prove that the observed class overrides the above method

Refer to this article to explore the implementation principle of KVC/KVO with code, through the code step by step analysis, from the breakpoint screenshots, can be very good proof of the above rewrite method.

Pay attention to my

Welcome to follow the public account: Jackyshan, technology dry goods first send wechat, the first time push.