Key-value coding is an easy-to-use tool for us in our programming life. one of the usages is to change between model and JSON which we use the third party libraries instead generally. But what's under the hood? Let's explore it.
First of all, let's have a look at the very easy demo and the model we will use:
- LGPerson:
// LGPerson.h
#import <Foundation/Foundation.h>
#import "LGStudent.h"
NS_ASSUME_NONNULL_BEGIN
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject{
@public
NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) LGStudent *student;
@end
// LGPerson.m
NS_ASSUME_NONNULL_END
#import "LGPerson.h"
@implementation LGPerson
@end
- LGStudent:
// LGStudent.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int stature;
@property (nonatomic, strong) NSMutableArray *penArr;
@end
NS_ASSUME_NONNULL_END
// LGStudent.m
#import "LGStudent.h"
@implementation LGStudent
@end
ViewController:
LGPerson *person = [[LGPerson alloc] init];
person.name = @"Justin";
What's the essence of person.name = @"Ray"
? Yes, it's the setter
created by compiler during the compiling time (no setter created by yourself). So we can access the setter
which stored in ro
by llvm
.
When we set a breakpoint on line 3. (with objc source code) We'll find the code will come here:
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}
tatic inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
This is the truth the code come here, but who called this function which encapsulat all the possible setter? Let's check another old friend llvm
. We can find some info here:
// CGObjcGNU.cpp
SetPropertyNonAtomicCopy.init(&CGM, "objc_setProperty_nonatomic_copy",
VoidTy, IdTy, SelectorTy, IdTy, PtrDiffTy);
// CGObjcMac.cpp
llvm::FunctionCallee getOptimizedSetPropertyFn(bool atomic, bool copy) {
CodeGen::CodeGenTypes &Types = CGM.getTypes();
ASTContext &Ctx = CGM.getContext();
// void objc_setProperty_atomic(id self, SEL _cmd,
// id newValue, ptrdiff_t offset);
// void objc_setProperty_nonatomic(id self, SEL _cmd,
// id newValue, ptrdiff_t offset);
// void objc_setProperty_atomic_copy(id self, SEL _cmd,
// id newValue, ptrdiff_t offset);
// void objc_setProperty_nonatomic_copy(id self, SEL _cmd,
// id newValue, ptrdiff_t offset);
SmallVector<CanQualType,4> Params;
CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
Params.push_back(IdType);
Params.push_back(SelType);
Params.push_back(IdType);
Params.push_back(Ctx.getPointerDiffType()->getCanonicalTypeUnqualified());
llvm::FunctionType *FTy =
Types.GetFunctionType(
Types.arrangeBuiltinFunctionDeclaration(Ctx.VoidTy, Params));
const char *name;
if (atomic && copy)
name = "objc_setProperty_atomic_copy";
else if (atomic && !copy)
name = "objc_setProperty_atomic";
else if (!atomic && copy)
name = "objc_setProperty_nonatomic_copy";
else
name = "objc_setProperty_nonatomic";
return CGM.CreateRuntimeFunction(FTy, name);
}
llvm::FunctionCallee GetOptimizedPropertySetFunction(bool atomic,
bool copy) override {
return ObjCTypes.getOptimizedSetPropertyFn(atomic, copy);
}
void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
const ObjCPropertyImplDecl *propImpl,
llvm::Constant *AtomicHelperFn) {
...
case PropertyImplStrategy::SetPropertyAndExpressionGet: {
llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
llvm::FunctionCallee setPropertyFn = nullptr;
if (UseOptimizedSetter(CGM)) {
// 10.8 and iOS 6.0 code and GC is off
setOptimizedPropertyFn =
CGM.getObjCRuntime().GetOptimizedPropertySetFunction(
strategy.isAtomic(), strategy.isCopy());
if (!setOptimizedPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C optimized setter - NYI");
return;
}
}
...
}
...
}
The process would be like in this work flow:
generateObjCSetterBody
GetOptimizedPropertySetFunction
getOptimizedSetPropertyFn
CreateRuntimeFunction(FTy, "objc_setProperty_nonatomic_copy")
objc_setProperty_nonatomic_copy
reallySetProperty
OK, that's the method to set name through setter
. We can also do it like this:
[person setValue:@"Justin_1" forKey:@"name"];
So what's the theory of this piece of code? Exploring through assembly is a tough job. Let's make it easy to use the Official Archieve File.
Hello, KVC
Acorrding to the file, We know that Key-value coding is a mechanism enabled by the NSKeyValueCoding
informal protocol that objects adopt to provide indirect access to their properties.
Basic visit
When we want to set the corner radius and some properties of layer, we can set them through keyPath
in Storyboard
like this;
Collection visit
Knowing that the KVO cann't observer the mutable collection, we will ask the KVC for help. See this:
person.array = @[@"1",@"2",@"3"];
what can I do to change the first element in the array
. You might want to do it like this:
person.array[0] = @"100";
Then you will get the error:
Expected method to write array element not found on object of type 'NSArray *'
The right way is do it like this:
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
Another way to do this is like:
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
You will get the result:
(
100,
2,
3
)
(
200,
2,
3
)
Collection Operators
- Dictionary:
NSDictionary* dict = @{
@"name":@"Justin",
@"nick":@"J",
@"subject":@"iOS",
@"age":@18,
@"stature":@180
};
LGStudent *p = [[LGStudent alloc] init];
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
Result:
<LGStudent: 0x600002e3b930>
{
age = 18;
name = Justin;
}
- Array:
NSArray *array = @[@"Ray",@"Justin",@"Jack",@"Peter"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr);
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);
Result:
(
3,
6,
4,
5
)
(
ray,
justin,
jack,
peter
)
- Aggregation Operator:
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *p = [LGStudent new];
NSDictionary* dict = @{
@"name": @"Tom",
@"age": @(18+i),
@"nick": @"Cat",
@"stature": @(175 + 2 * arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"stature"]);
float avg = [[personArray valueForKeyPath:@"@avg.stature"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.stature"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.stature"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.stature"] intValue];
NSLog(@"%d", max);
int min = [[personArray valueForKeyPath:@"@min.stature"] intValue];
NSLog(@"%d", min);
Result:
178.333328
6
1070
181
175
- Array Operator:
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *p = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"stature":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"stature"]);
NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.stature"];
NSLog(@"arr1 = %@", arr1);
NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.stature"];
NSLog(@"arr2 = %@", arr2);
Result:
(
181,
183,
181,
181,
179,
179
)
arr1 = (
181,
183,
181,
181,
179,
179
)
arr2 = (
181,
183,
179
)
- Array Nesting:
NSMutableArray *personArray1 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *student = [LGStudent new];
NSDictionary* dict = @{
@"name": @"Tom",
@"age": @(18+i),
@"nick": @"Cat",
@"stature": @(175 + 2*arc4random_uniform(6)),
};
[student setValuesForKeysWithDictionary:dict];
[personArray1 addObject:student];
}
NSMutableArray *personArray2 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name": @"Tom",
@"age": @(18+i),
};
[person setValuesForKeysWithDictionary:dict];
[personArray2 addObject:person];
}
NSArray* nestArr = @[personArray1, personArray2];
NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.name"];
NSLog(@"arr = %@", arr);
NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.age"];
NSLog(@"arr1 = %@", arr1);
Result is:
arr = (
Tom
)
arr1 = (
18,
19,
20,
21,
22,
23,
18,
19,
20,
21,
22,
23
)
- Nest:
NSMutableSet *personSet1 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
LGStudent *person = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18 + i),
@"nick":@"Cat",
@"stature":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personSet1 addObject:person];
}
NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"age"]);
NSMutableSet *personSet2 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18 + i * 2),
};
[person setValuesForKeysWithDictionary:dict];
[personSet2 addObject:person];
}
NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"age"]);
NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];
NSSet* set1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.age"];
NSLog(@"set1 = %@", set1);
NSLog(@"arr1 = %@", [set1 allObjects]);
Result:
personSet1 = {(
21,
20,
23,
19,
22,
18
)}
personSet2 = {(
28,
24,
20,
26,
22,
18
)}
set1 = {(
26,
22,
18,
23,
19,
28,
24,
20,
21
)}
arr1 = (
26,
22,
18,
23,
19,
28,
24,
20,
21
)
Non-Object Type
How to set ThreeFloats
? Maybe you will try this:
ThreeFloats floats = {1., 2., 3.};
[person setValue:floats forKey:@"threeFloats"];
and get the error:
Sending 'ThreeFloats' to parameter of incompatible type 'id _Nullable'
We should do like this:
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);
ThreeFloats th;
[reslut getValue:&th];
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
Result:
{length = 12, bytes = 0x0000803f0000004000004040}
1.000000 - 2.000000 - 3.000000
Object Nesting
LGStudent *student = [[LGStudent alloc] init];
student.subject = @"iOS";
person.student = student;
[person setValue:@"KVC theory" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
Result:
KVC theory
setValue:forKey:
in KVC
Prepare code:
// LGPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
NS_ASSUME_NONNULL_END
// LGPerson.m
#import "LGPerson.h"
@implementation LGPerson
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
in ViewController:
LGPerson *person = [[LGPerson alloc] init];
[person setValue:@"Justin" forKey:@"name"];
According to the official file, we know the process is like this:
-
Look for the first accessor named
set<Key>:
or_set<Key>
, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.Run the code and get the result:
-[LGPerson setName:] - Justin
comment this piece of code:
//- (void)setName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); //}
Run the code:
-[LGPerson _setName:] - Justin
comment this piece of code:
//- (void)_setName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); //}
Result:
-[LGPerson setIsName:] - Justin
comment this piece of code:
//- (void)setIsName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); //}
-
If no simple accessor is found, and if the class method
accessInstanceVariablesDirectly
returnsYES
, look for an instance variable with a name like_<key>
,_is<Key>
,<key>
, oris<Key>
, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.So let's try this first:
+ (BOOL)accessInstanceVariablesDirectly{ return NO; }
Result is:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x600003238dc0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
Oops! Change it back to
YES
and add code inViewController
:NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
Run the code and result is:
Justin-(null)-(null)-(null)
comment the
_name
and change code inViewController
to:NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
Check the result:
Justin-(null)-(null)
comment the
_isName
and change code inViewController
to:NSLog(@"%@-%@",person->name,person->isName);
Result:
Justin-(null)
comment the
name
and change code inViewController
to:NSLog(@"%@",person->isName);
result is:
Justin
-
Upon finding no accessor or instance variable, invoke
setValue:forUndefinedKey:
. This raises an exception by default, but a subclass ofNSObject
may provide key-specific behavior.comment the
isName
, you will get the error:*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x6000037371e0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
The design makes the KVC so great to fault-tolerant.
getValueForKey:
in KVC
After setting the value to the member variable, we are going to get them. So first prepare the value for them:
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
Of course you should also uncomment the variable:
@interface LGPerson : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
Then let's go through with the process from the official file in:
-
Search the instance for the first accessor method found with a name like
get<Key>
,<key>,
,is<Key>
, or_<key>
, in that order.Run code and check the result:
get value:getName
Comment:
//- (NSString *)getName{ // return NSStringFromSelector(_cmd); //}
Run:
get value:name
Comment:
//- (NSString *)name{ // return NSStringFromSelector(_cmd); //}
Run:
get value:isName
Comment:
//- (NSString *)isName{ // return NSStringFromSelector(_cmd); //}
Run:
get value:_name
Comment:
//- (NSString *)_name{ // return NSStringFromSelector(_cmd); //}
-
If no simple accessor method is found, search the instance for methods whose names match the patterns
countOf
andobjectInAtIndex:
(corresponding to the primitive methods defined by theNSArray
class) andAtIndexes:
(corresponding to theNSArray
methodobjectsAtIndexes:
).If the first of these and at least one of the other two is found, create a collection proxy object that responds to all
NSArray
methods and return that. Otherwise, proceed to step 3.The proxy object subsequently converts any
NSArray
messages it receives to some combination ofcountOf
,objectInAtIndex:
, andAtIndexes:
messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name likeget:range:
, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were anNSArray
, even if it is not.add these two methods in
LGPerson.m
// count - (NSUInteger)countOfPens{ NSLog(@"%s",__func__); return [self.arr count]; } // item - (id)objectInPensAtIndex:(NSUInteger)index { NSLog(@"%s",__func__); return [NSString stringWithFormat:@"pens %lu", index]; }
in
ViewController.m
:NSLog(@"get value:%@",[person valueForKey:@"pens"]);
You will get the result;
get value:( )
we can also test like this:
person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"]; NSArray *array = [person valueForKey:@"pens"]; NSLog(@"%@",[array objectAtIndex:1]); NSLog(@"%d",[array containsObject:@"pen1"]);
Result:
-[LGPerson objectInPensAtIndex:] pens 1 -[LGPerson countOfPens] -[LGPerson countOfPens] -[LGPerson objectInPensAtIndex:] -[LGPerson objectInPensAtIndex:] -[LGPerson objectInPensAtIndex:] -[LGPerson objectInPensAtIndex:] 0
-
If no simple accessor method or group of array access methods is found, look for a triple of methods named
countOf
,enumeratorOf
, andmemberOf:
(corresponding to the primitive methods defined by theNSSet
class).If all three methods are found, create a collection proxy object that responds to all
NSSet
methods and return that. Otherwise, proceed to step 4.This proxy object subsequently converts any
NSSet
message it receives into some combination ofcountOf
,enumeratorOf
, andmemberOf:
messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were anNSSet
, even if it is not.comment the above methods
//- (NSUInteger)countOfPens{ // NSLog(@"%s",__func__); // return [self.arr count]; //} //- (id)objectInPensAtIndex:(NSUInteger)index { // NSLog(@"%s",__func__); // return [NSString stringWithFormat:@"pens %lu", index]; //}
add the following code:
- (NSUInteger)countOfBooks{ NSLog(@"%s",__func__); return [self.set count]; } - (id)memberOfBooks:(id)object { NSLog(@"%s",__func__); return [self.set containsObject:object] ? object : nil; } - (id)enumeratorOfBooks { // objectEnumerator NSLog(@"here enumerate"); return [self.arr reverseObjectEnumerator]; }
in
ViewController.m
NSLog(@"get value:%@",[person valueForKey:@"books"]);
get the result:
get value:{( )}
you can also try this:
person.set = [NSSet setWithArray:person.arr]; NSSet *set = [person valueForKey:@"books"]; [set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) { NSLog(@"set enumerate %@",obj); }];
you'll get the result:
2020-02-11 13:53:52.208905+0800 002-KVC取值&赋值过程[3488:199892] -[LGPerson countOfBooks] 2020-02-11 13:53:52.209016+0800 002-KVC取值&赋值过程[3488:199892] -[LGPerson countOfBooks] 2020-02-11 13:53:52.209110+0800 002-KVC取值&赋值过程[3488:199892] here enumerate 2020-02-11 13:53:52.209229+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen3 2020-02-11 13:53:52.209342+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen2 2020-02-11 13:53:52.209550+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen1 2020-02-11 13:53:52.209839+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen0
-
If no simple accessor method or group of collection access methods is found, and if the receiver's class method
accessInstanceVariablesDirectly
returnsYES
, search for an instance variable named_<Key>
,_is<Key>
,<key>
, oris<Key>
, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.Still ensure
accessInstanceVariablesDirectly
isYES
:+ (BOOL)accessInstanceVariablesDirectly{ return YES; }
Run the code:
get value:_name
comment the:
// NSString *_name; and // person->_name = @"_name";
Run:
get value:_isName
comment the:
// NSString *_isName; and // person->_isName = @"_isName";
Run:
get value:name
comment the:
// NSString *name; and // person->name = @"name";
Run:
get value:isName
Comment the:
// NSString *isName; and // person->isName = @"isName";
-
If the retrieved property value is an object pointer, simply return the result. If the value is a scalar type supported by
NSNumber
, store it in anNSNumber
instance and return that. If the result is a scalar type not supported by NSNumber, convert to anNSValue
object and return that.Here is the LGStudent
// LGStudent.h #import <Foundation/Foundation.h> typedef struct { float x, y, z; } ThreeFloats; NS_ASSUME_NONNULL_BEGIN @interface LGStudent : NSObject{ int age; ThreeFloats threeFloats; } @end NS_ASSUME_NONNULL_END // LGStudent.m @implementation LGStudent @end
in ViewController.m
LGStudent *student = [[LGStudent alloc] init]; student->age = 18; ThreeFloats th = {1.f, 2.f, 3.f}; student->threeFloats = th; id retAge = [student valueForKey:@"age"]; id retThreeFloats = [student valueForKey:@"threeFloats"]; NSLog(@"get value:%@",retAge); NSLog(@"get value:%@",retThreeFloats);
get the result:
img_2.pngget value:18 get value:{length = 12, bytes = 0x0000803f0000004000004040}
-
If all else fails, invoke
valueForUndefinedKey:
. This raises an exception by default, but a subclass ofNSObject
may provide key-specific behavior.from the result of 4, run the code, you will get
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x6000029ccb10> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
Customize KVC
Now we have learned the base process of the KVC, let's do it by ourselves (easy one) !
First of all, we should check the entry:
@interface NSObject(NSKeyValueCoding)
So here is the hint we can write our code in the category of NSObject
, it's a good idea to decoupling (like you can do that of AppDelegate). Let's create a category:
// .h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LGKVC)
// custom KVC entry
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)lg_valueForKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
// .m
#import "NSObject+LGKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LGKVC)
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
}
- (nullable id)lg_valueForKey:(NSString *)key{
}
then let's make a todo list for set
:
- check the
key
isnil
(return) or not. - check the
setter
. - check the
accessInstanceVariablesDirectly
-NO
(throw exception) - check whether the variable in the ivar list.
- set the variable (setIvar) if has otherwise throw exception.
Let's write the code:
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- put all the
setter
together
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
- get iVar list.
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// 1: check the nil
if (key == nil || key.length == 0) return;
// 2: check the methods set<Key> _set<Key> setIs<Key>
// key need Capitalized
NSString *Key = key.capitalizedString;
// connect the string
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3:check can set variable directly
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.get the variable to set
// 4.1 get ivar list
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 4.2 get the corresponding ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 set value to ivar
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5: ivars not found
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
- The custom set value for key method.
let's make a todo list for getter
:
- check the key
nil
(return) or not. - check the
getter
(array
, currently ignore theset
for convenience). - check the
accessInstanceVariablesDirectly
-NO
(throw exception) - check whether the variable in the ivar list.
- Return the variable (setIvar) if has otherwise return "".
Let's write the code:
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- for
getter
.
- (nullable id)lg_valueForKey:(NSString *)key{
// 1:check key nil or not
if (key == nil || key.length == 0) {
return nil;
}
// 2:fins associate methods get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key need capitalized
NSString *Key = key.capitalizedString;
// connect the string
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:whether achieve the variable directly
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.return the ivar
// 4.1 get the ivar list
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
This is the base version of the KVC. There's a version quite detail which is written by other code, the project name is DIS_KVC_KVO. We can learn the thoughts there.
Conclude and Tips
We know that we usually set the Int
value to the type Int
, but in KVC, we can also use NSString
:
@property (nonatomic, assign) int age;
LGPerson *person = [[LGPerson alloc] init];
[person setValue:@18 forKey:@"age"];
[person setValue:@"20" forKey:@"age"]; // int - string
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
But this time the type of valueForKey:
is __NSCFNumber
. KVC can auto change the type for you.
Bool
is the same:
@property (nonatomic, assign) BOOL sex;
[person setValue:@"20" forKey:@"sex"];
NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber
struct
is like this:
typedef struct {
float x, y, z;
} ThreeFloats;
@property (nonatomic) ThreeFloats threeFloats;
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue
What about the nil
[person setValue:nil forKey:@"age"]; // only for NSNumber - NSValue
[person setValue:nil forKey:@"subject"]; // will not get into the next line of code
with
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"Are u kidding me: set %@ to nil?",key);
}
the second line will not get into the following part of code.
/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. The default implementation of this method raises an NSInvalidArgumentException. You can override it to map nil values to something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;
No key setter:
[person setValue:nil forKey:@"HH"];
// will get into:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"what are u doing: %@ has not this key",key);
}
No key getter:
NSLog(@"%@",[person valueForKey:@"HH"]);
// will get into:
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"Hey: %@ has not this key - give u another one!",key);
return @"Master";
}
Validate:
NSError *error;
NSString *name = @"LG_Cooci";
if (![person validateValue:&name forKey:@"names" error:&error]) {
NSLog(@"%@",error);
}else{
NSLog(@"%@",[person valueForKey:@"name"]);
}
// will get into:
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"we change it here: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ is not the property of %@",inKey,self] code:10088 userInfo:nil];
return NO;
}
looks like we can redirect the value to some key here or fault-tolerant or forward. For more detail info you can check the official file.
网友评论