Key-value coding is the ability to read and set a property using its name. The key-value coding methods are defined in NSObject, and thus every object has this capability.
Open main.m and find the line:
[a setProductName:@"Washing Machine"];
Rewrite the same line to use key-value coding:
[a setValue:@"Washing Machine" forKey:@"productName"];
In this case, the setValue:forKey: method, as defined in NSObject, will go looking for a setter method named setProductName:. If the object does not have a setProductName: method, it will access the instance variable directly.
You can also read the value of a variable using key-value coding. Add a line to main.m that prints out the product name:
int main (int argc, const char * argv[]) { @autoreleasepool { BNRAppliance *a = [[BNRAppliance alloc] init]; NSLog(@"a is %@", a); [a setValue:@"Washing Machine" forKey:@"productName"]; [a setVoltage:240]; NSLog(@"a is %@", a); NSLog(@"the product name is %@", [a valueForKey:@"productName"]); } return 0; }
In this case, the valueForKey: method, as defined in NSObject, goes looking for an accessor named productName. If there is no productName method, the instance variable is accessed directly.
This use of the word “key” seems to bother some readers. You can imagine the problem: the engineer who named these methods needed a word that could mean the name of an instance variable or the name of a property or the name of a method. “Key” was the most specific word that he could come up with.
If you type the name of the property wrong, you will not get a warning from the compiler, but there will be a runtime error. Make this mistake in main.m:
NSLog(@"the product name is %@", [a valueForKey:@"productNammmme"]);
When you build and run it, you will see an error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<BNRAppliance 0x100108dd0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key productNammmme.'
Fix the error before you go on.
Why is key-value coding interesting? Anytime a standard framework wants to push data into your objects, it will use setValue:forKey:. Anytime a standard framework wants to read data from your objects, it will use valueForKey:. For example, Core Data is a framework that makes it easy to save your objects to a SQLite database and then bring them back to life. It manipulates your custom data-bearing objects using key-value coding.
To prove that key-value coding will manipulate your variables even if you have no accessors, explicitly declare an instance variable for the productName and comment out the @property declaration for productName in BNRAppliance.h:
#import <Foundation/Foundation.h> @interface BNRAppliance : NSObject { NSString *_productName; } // @property (nonatomic, copy) NSString *productName; @property (nonatomic) int voltage; // The designated initializer - (instancetype)initWithProductName:(NSString *)pn; @end
Also, remove all use of the methods setProductName: and productName from BNRAppliance.m:
@implementation BNRAppliance - (instancetype)initWithProductName:(NSString *)pn { if (self = [super init]) { _productName = [pn copy]; _voltage = 120; } return self; } - (instancetype)init { return [self initWithProductName:@"Unknown"]; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %d volts>", _productName, self.voltage]; } @end
Build and run the program. Note that even though you have no accessor methods for productName, the variable can still be set and read from other methods. This is an obvious violation of the idea of object encapsulation – methods of an object are public, but the instance variables are delicate and should be kept private. If key-value coding was not astonishingly useful, no one would tolerate it.
The key-value coding methods are designed to work with objects, but some properties hold a non-object type, like an int or a float. For example, voltage is an int. How do you set voltage using key-value coding? You use an NSNumber.
In main.m, change the line for setting the voltage from this:
[a setVoltage:240];
to this:
[a setValue:[NSNumber numberWithInt:240] forKey:@"voltage"];
Add an explicit accessor to BNRAppliance.m so that you can see it getting called:
- (void)setVoltage:(int)x { NSLog(@"setting voltage to %d", x); _voltage = x; }
Build and run the program.
Similarly, if you ask for the valueForKey:@"voltage", you will get back an NSNumber containing the value of voltage.