Published

NSCoding, Without the Boilerplate

In Object Serialization With NSCoding we talked about how your can use the NSKeyedArchiver and the NSCoding protocol to easily save your custom model objects as Binary Plists for later retrieval. As a reminder, here is how we would implement NSCoding for a simple class “Foo”, with three properties:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

@interface Foo : NSObject<NSCoding>

@property(nonatomic,assign)NSIntegerproperty1;

@property(nonatomic,assign)BOOLproperty2;

@property(nonatomic,copy)NSString*property3;

@end

@implementationFoo

-(id)initWithCoder:(NSCoder*)coder

{

if((self=[superinit]))

{

// Decode the property values by key,

// and assign them to the correct ivars

_property1=[coder decodeIntegerForKey:@"property1"];

_property2=[coder decodeBoolForKey:@"property2"];

_property3=[coder decodeObjectForKey:@"property3"];

}

returnself;

}

-(void)encodeWithCoder:(NSCoder*)coder

{

// Encode our ivars using string keys

[coder encodeInteger:_property1 forKey:@"property1"];

[coder encodeBool:_property2 forKey:@"property2"];

[coder encodeObject:_property3 forKey:@"property3"];

}

@end

NSCoding eliminates a lot of the complexity of saving objects by automatically handling circular references, duplicate objects, etc. But you still have to write those pesky initWithCoder: and encodeWithCoder: methods for every class. That’s kinds of a drag. Implementing NSCoding can involve a lot of boilerplate, especially if your object has a lot of properties to encode. Is there anything we can do about that?

Work Smarter, Not Harder

Let’s look at what we actually need to do for each property that we’re encoding. For each property we’ve written the following:

1

2

3

4

5

// In the initWithCoder: method

self.someProperty=[coder decodeObjectForKey:@"someProperty"];

// In the encodeWithCoder: method

[coder encodeObject:self.someProperty forKey:@"someProperty"];

That’s four references to the name of our property. Two of the references are selectors, two of them are strings. Not very DRY. We need to find some way to eliminate this repetition.

We can make use of KVC (Key-Value Coding) to set and get properties by name. KVC also has the neat feature that it can automatically box and unbox primitive values such as integers or booleans into their equivalent object type (e.g. NSNumber), so we don’t have to worry about using different methods to encode our properties, we can treat them all as objects. Using KVC we can rewrite our encode/decode calls as follows:

1

2

3

4

5

6

7

8

9

NSString*constPropertyName=@"someProperty";

// In the initWithCoder: method

idvalue=[coder decodeObjectForKey:PropertyName];

[self setValue:value forKey:PropertyName];

// In the encodeWithCoder: method

idvalue=[self valueForKey:PropertyName];

[coder encodeObject:value forKey:PropertyName];

Because we’ve eliminated any references to the specific property name in our code, we can now make it reusable. By looping through an array of property names, we can encode/decode all our properties easily. That means we can create a common base class for all our codable objects that does most of the work for us:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

@interface CodableObject : NSObject<NSCoding>

-(NSArray*)propertyNames;

@end

@implementationCodableObject

-(NSArray*)propertyNames

{

// Override this in the subclass to return an array of property names

returnnil;

}

-(id)initWithCoder:(NSCoder*)aDecoder

{

if((self=[selfinit]))

{

// Loop through the properties

for(NSString*keyin[selfpropertyNames])

{

// Decode the property, and use the KVC setValueForKey: method to set it

idvalue=[aDecoder decodeObjectForKey:key];

[self setValue:value forKey:key];

}

}

returnself;

}

-(void)encodeWithCoder:(NSCoder*)aCoder

{

// Loop through the properties

for(NSString*keyin[selfpropertyNames])

{

// Use the KVC valueForKey: method to get the property and then encode it

idvalue=[self valueForKey:key];

[aCoder encodeObject:value forKey:key];

}

}

@end

To implement our NSCodable Foo class from earlier, all we have to do now is this:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@interface Foo : CodableObject

@property(nonatomic,assign)NSIntegerproperty1;

@property(nonatomic,assign)BOOLproperty2;

@property(nonatomic,copy)NSString*property3;

@end

@implementationFoo

-(NSArray*)propertyNames

{

return@[@"property1",@"property2",@"property3"];

}

@end

So much nicer! But if we can reduce our encoding down to an array of property names, can’t we just get Cocoa to figure that bit out for us too?

Introspection FTW!

The objective-C runtime can help us out here. Using a bit of runtime magic, we can find out the names of all the properties of our class and generate the propertyNames array automatically in our CodableObject class. Here’s the code to do that:

Now any object that inherits from CodableObject supports NSCoding automatically without needing to override the propertyNames method. Sweet! There are a few caveats though:

1) This mechanism won’t encode properties inherited from a superclass. CodableObject doesn’t have any properties, but if we had a deeper inheritance structure (e.g. Dog > Animal > CodableObject) then the properties inherited from the Animal class wouldn’t get coded when we save an object of class Dog.

2) Not every property can be NSCoded. If a property is both virtual (not backed by an ivar) and readonly, it can’t be set using setValueForKey:, which means it will crash our initWithCoder: method. If it’s readonly and has an ivar, but the ivar name doesn’t match the property name, setValueForKey: won’t work either. Encoding readwrite virtual properties will work, but it’s probably pointless – if they have no backing ivar, they are probably computed values and don’t need to be saved. To be safe, we really only want to encode properties that have a backing ivar with the same name.

3) Not every property will be codable. If it’s something like a struct or pointer, or an object which doesn’t itself support NSCoding, it won’t work.

4) Sometimes we may want to omit properties from coding. For example if our class contains something like a timer, or some flags used purely for tracking runtime state, we may not wish to save those.

To fix issue 1 we need to loop through the object’s superclasses and make sure we capture all the properties in those classes too. To fix issue 2 we need to check some extra metadata about each property. Our improved propertyNames method looks like this:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

-(NSArray*)propertyNames

{

// Loop through our superclasses until we hit NSObject

NSMutableArray*array=[NSMutableArrayarray];

Classsubclass=[selfclass];

while(subclass!=[NSObjectclass])

{

unsignedintpropertyCount;

objc_property_t *properties=class_copyPropertyList(subclass,

&propertyCount);

for(inti=0;i<propertyCount;i++)

{

// Get property name

objc_property_t property=properties[i];

constchar*propertyName=property_getName(property);

NSString*key=@(propertyName);

// Check if there is a backing ivar

char*ivar=property_copyAttributeValue(property,"V");

if(ivar)

{

// Check if ivar has KVC-compliant name

NSString*ivarName=@(ivar);

if([ivarName isEqualToString:key]||

[ivarName isEqualToString:[@"_" stringByAppendingString:key]])

{

// setValue:forKey: will work

[array addObject:key];

}

free(ivar);

}

}

free(properties);

subclass=[subclass superclass];

}

returnarray;

}

That solves issue 1 and 2 and should now work for all common cases. There isn’t any simple solution to 3, but it’s not a big deal because we can avoid adding properties to our classes that can’t be auto-coded in most cases, and when we can’t, we can just override the NSCoding methods and handle them manually as a special case.

What about issue 4? How can we omit properties from being coded? If the property is private, we can just declare an ivar and no property declaration. We only loop through properties, so ivars with no associated property won’t be encoded.

For public properties, we can always override our propertyNames array to omit properties we don’t want to save, but that’s a bit messy. Fortunately our solution to problem 2 gives us a simple way to exclude certain properties from coding. We exclude properties from the propertyNames array if their ivar doesn’t match the name of the property. We do this out of technical necessity, but we can take advantage of it here. If we add a synthesise statement that defines the property as having a non-compliant name, it won’t be encoded. E.g.

1

2

3

4

@synthesizefoo=foo;// This *will* be encoded

@synthesizefoo=_foo;// So will this

@synthesizefoo=foo_;// But this *won't* be

@synthesizefoo=bar;// Nor will this

One last thing: The propertyNames method is pretty fast, but it seems inefficient to call it every time we encode or decode a class since the property names are decided at compile time and aren’t likely to ever change during program execution. Is there some way we can cache the propertyNames array for each class (preferably without needing to add extra properties or methods to our subclasses)?

Cool By Association

In Adding Properties to a Category Using Associated Objects we talked about using associated objects to add extra data to existing objects. Are you thinking what I’m thinking? No? OK, well I’m thinking we should use the associated objects mechanism to store our propertyNames array in the class once it has been calculated for the first time. You can do that as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

-(NSArray*)propertyNames

{

// Check for a cached value (we use _cmd as the cache key,

// which represents @selector(propertyNames))

NSMutableArray*array=objc_getAssociatedObject([selfclass],_cmd);

if(array)

{

returnarray;

}

// Loop through our superclasses until we hit NSObject

array=[NSMutableArrayarray];

Classsubclass=[selfclass];

while(subclass!=[NSObjectclass])

{

unsignedintpropertyCount;

objc_property_t *properties=class_copyPropertyList(subclass,

&propertyCount);

for(inti=0;i<propertyCount;i++)

{

// Get property name

objc_property_t property=properties[i];

constchar*propertyName=property_getName(property);

NSString*key=@(propertyName);

// Check if there is a backing ivar

char*ivar=property_copyAttributeValue(property,"V");

if(ivar)

{

// Check if ivar has KVC-compliant name

NSString*ivarName=@(ivar);

if([ivarName isEqualToString:key]||

[ivarName isEqualToString:[@"_" stringByAppendingString:key]])

{

// setValue:forKey: will work

[array addObject:key];

}

free(ivar);

}

}

free(properties);

subclass=[subclass superclass];

}

// Cache and return array

objc_setAssociatedObject([selfclass],_cmd,array,

OBJC_ASSOCIATION_RETAIN_NONATOMIC);

returnarray;

}

And there we have it. Automatic NSCoding for all your classes, without the boilerplate.