Subclassing Those Hard-to-Reach Classes
Ah, read-only properties. They are the bane of my existence when I find myself trying to think outside of the box as an iOS developer. Oh, UINavigationController, why must you taunt me with your read-only navigation bar and toolbar? And you, UIWebView. Is your scroll view really so dangerous that you won’t let me override it?
There have been many times when I have wanted to slightly customize the behavior of an object that is completely owned by its parent. This can be an incredibly difficult task to accomplish. For example, when I wanted to add a background image to a UINavigationController’s navigation bar, I unfortunately found myself resorting to method swizzling to accomplish my task. Now don’t get me wrong, I know method swizzling can be dangerous, and, if you utilize it, you need to be very careful to test your code before each and every release of iOS or OS X. However, if used sparingly and carefully, it can be a powerful, last-resort technique for altering a class that you can’t easily subclass.
Yet I recently found another way to modify objects that appear unmodifiable. There is a lesser-known feature of NSKeyedUnarchiver that lets you change the class of an object that has already been allocated and initialized. NSKeyedArchiver and NSKeyedUnarchiver are generally used to serialize objects to disk; but they can also be used for other purposes.
Going back to the UINavigationBar background image problem, what I really wanted to do was subclass UINavigationBar and tell the UINavigationController to use that class for its navigation bar. However, since
navigationBar is a read-only property of UINavigationController there appeared to be little I could do. NSKeyedUnarchiver can help!
// Create a run-of-the-mill UINavigationController. UINavigationController *nc = [[UINavigationController alloc] initWithNibName:nil bundle:nil]; // Ensure the UINavigationBar is created so that it can be properly archived. // If we do not access the navigation bar then it will not be allocated, and // thus, it will not be archived by the NSKeyedArchvier. [nc navigationBar]; // Archive the navigation controller. NSMutableData *data = [NSMutableData data]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; [archiver encodeObject:nc forKey:@"root"]; [archiver finishEncoding]; [archiver release]; [nc release]; // Unarchive the navigation controller and ensure that our UINavigationBar // subclass is used. NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; [unarchiver setClass:[SCNavigationBar class] forClassName:@"UINavigationBar"]; nc = [unarchiver decodeObjectForKey:@"root"]; [unarchiver finishDecoding]; [unarchiver release];
In the code above,
setClass:forClassName: tells the NSKeyedUnarchiver to actually use the SCNavigationBar class whenever it tries to decode an object of class UINavigationBar. Since SCNavigationBar is just a subclass that I created of UINavigationBar, the unarchiving process works without a hitch.
So, by the end of that code block,
nc points to a navigation controller with an instance of SCNavigationBar for its
navigationBar property! This allows us to customize the behavior of the navigation bar to our heart’s content without having to resort to method swizzling. The one catch is that this technique will only work for container classes that conform to the NSCoding protocol. Luckily for us, most of Apple’s classes do.
Because I find this technique to be much cleaner than method swizzling, I have updated my ExampleNavBarBackground sample project to use NSKeyedUnarchiver, instead. Happy coding!