nskeyedunarchiver

Harm-free Class Swizzling with NSKeyedUnarchiver

I came across a problem where we need to subclass UINavigationBar in an UINavigationController. Conventionally, one may subclass the navigation controller, and commence hacks. Fortunately, NSKeyedArchiver saves the day:

self.window.rootViewController = (( ^ {

    //  Since it is totally unsafe to modify the navigation controller, the best way to swizzle-in a custom navigation bar subclass is to use NSKeyedUnarchiver, providing a custom class for any object whose class is named UINavigationBar.

    UINavigationController *navController = [[[UINavigationController alloc] initWithRootViewController:presentedViewController] autorelease];
    NSData *navControllerData = [NSKeyedArchiver archivedDataWithRootObject:navController];

    NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:navControllerData] autorelease];
    [unarchiver setClass:[WANavigationBar class] forClassName:@"UINavigationBar"];

    //  The root object is keyed “root”. :D
    UINavigationController *swizzledNavController = [unarchiver decodeObjectForKey:@"root"];
    [swizzledNavController setViewControllers:navController.viewControllers];

    return swizzledNavController;

})());

WANavigationBar inherits from UINavigationBar, and overrides -drawRect: with an empty implementation.

Obviously, if you were already setting your navigation controller using Interface Builder (i.e. you can actually see it in Interface Builder), then the best way requiring no swizzling code at all is to find the navigation bar instance, then set its class to your own subclass in the Identity inspector.

Dynamic NSCoding with Objective-C runtime and NSKeyedArchivier NSKeyedUnarchivier

The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving and distribution. In other words, the basis for serialize and deserialize objects.

Use the NSCoding is easy.

Your class must implement the NSCoding protocol into the .h file. 
Into the .m you must implement the methods initWithCode: and encodeWithCoder:.

That is like below:

#import <Foundation/Foundation.h>

@interface Organism : NSObject <NSCoding>

//properties goes here

-(void)saveToDiskWithKey:(NSString *)key;
+(id)loadFromDiskWithKey:(NSString *)key;

@end
#import "Organism.h"

@implementation Organism

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
       //code goes here
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    //code gose here
}

So, using the class NSCoding, the class NSKeyedArchivier and the class NSKeyedUnarchivier, you can write and read objects into the iOS application sandbox.

Well, suppose that you have two classes with different properties. For example:

Human” with name, surname, age;

#import <Foundation/Foundation.h>

@interface Human : Organism 

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *surname;
@property (nonatomic, strong) NSString *age;

@end

Cat” with name, pedigree.

#import <Foundation/Foundation.h>

@interface Cat : Organism 

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *pedigree;

@end

Human and Cat inherits from a third class named “Organism”.

Without know if you are a Human or a Cat, you can encode and decode your classes, dynamically, using the Objective-c Runtime library, following the code below (Organism.m).

#import "Organism.h"
#import <objc/message.h>  //import the runtime library

@implementation Organism

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        
        unsigned int pCounter = 0;
        objc_property_t *properties = class_copyPropertyList([self class], &pCounter);
        
        for (unsigned int i = 0; i < pCounter; i++)
        {
            objc_property_t prop = properties[i];
            const char *propName = property_getName(prop);
            NSString *pUTF8 = [NSString stringWithUTF8String:propName];
            
            [self setValue:[aDecoder decodeObjectForKey:pUTF8] forKey:pUTF8];
        }

        free(properties);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    unsigned int pCounter = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &pCounter);
    
    for (unsigned int i = 0; i < pCounter; i++)
    {
        objc_property_t prop = properties[i];
        const char *propName = property_getName(prop);
        NSString *pUTF8 = [NSString stringWithUTF8String:propName];
        
        [aCoder encodeObject:[self valueForKey:pUTF8] forKey:pUTF8];
    }
    
    free(properties);
}

It will encode and decode your properties using their names. Amazing!!

So now you can read and write your encoded classes from and to the disk, using two custom methods (have you seen saveToDiskWithKey: and loadFromDiskWithKey: above??), into the Organism.m class:

#import "Organism.h"
#import <objc/message.h>  //import the runtime library

@implementation Organism

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        
        unsigned int pCounter = 0;
        objc_property_t *properties = class_copyPropertyList([self class], &pCounter);
        
        for (unsigned int i = 0; i < pCounter; i++)
        {
            objc_property_t prop = properties[i];
            const char *propName = property_getName(prop);
            NSString *pUTF8 = [NSString stringWithUTF8String:propName];
            
            [self setValue:[aDecoder decodeObjectForKey:pUTF8] forKey:pUTF8];
        }

        free(properties);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    unsigned int pCounter = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &pCounter);
    
    for (unsigned int i = 0; i < pCounter; i++)
    {
        objc_property_t prop = properties[i];
        const char *propName = property_getName(prop);
        NSString *pUTF8 = [NSString stringWithUTF8String:propName];
        
        [aCoder encodeObject:[self valueForKey:pUTF8] forKey:pUTF8];
    }
    
    free(properties);
}

-(void)saveToDiskWithKey:(NSString *)key
{
	NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
	[[NSUserDefaults standardUserDefaults] setObject:data forKey:key];
	[[NSUserDefaults standardUserDefaults] synchronize];
}

+(id)loadFromDiskWithKey:(NSString *)key
{
	NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:key];
	id result = [NSKeyedUnarchiver unarchiveObjectWithData:data];
	return result;
}

That’s all!

You are ready to load and save Human and Cat:

Human *human = [Human loadFromDiskWithKey:@"thehuman"];
[human saveToDiskWithKey:@"thehuman"];

Cat *cat = [Cat loadFromDiskWithKey:@"thecat"];
[cat saveToDiskWithKey:@"thecat"];
Harmless UINavigationController Swizzling — Redux

Forget all the hard work swizzling stuff and making things conform to NSCoding, it’s not always easy anyway. You can simply call -initWithRootController: twice. That’s the spirit.

UIViewController *emptyVC = [[[UIViewController alloc] init] autorelease];
__block WANavigationController *navController = [[[WANavigationController alloc] initWithRootViewController:emptyVC] autorelease];
navController = [[((^ {
    NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[NSKeyedArchiver archivedDataWithRootObject:navController]] autorelease];
    [unarchiver setClass:[WANavigationBar class] forClassName:@"UINavigationBar"];
    [unarchiver setClass:[UIViewController class] forClassName:NSStringFromClass([navController.topViewController class])];
    return unarchiver;
})()) decodeObjectForKey:@"root"] initWithRootViewController:navController.topViewController];

If the (intended) root view controller was used to initialize the old navigation controller, its navigationController property will be totally messed up when it is pushed again to a new navigation controller. Even if the property passes an assertion. The idea is to hand a fake one to the old navigation controller since everything that is not related to the navigation controller’s view is not that important.