NSAssert? What? Why? (skip if you’re familiar with this)
During development, it’s standard “best practice” to us NSAssert liberally, alerting you early if you have unexpected bugs in your code, for instance:
-(NSString*) processItem:(int) index
{
NSAssert( index < [myArray count], @"Index is bigger than number of items in the myArray array" );
...
... // rest of the method goes here
}
Unit Tests are an even better approach, but there are situations – e.g. complex algorithms – where you want to perform sanity-checks in the middle of code.
NSAssert in Xcode (Apple’s default setup for new projects)
Apple has hooked up NSAssert so that in Debug builds (by default: anything on the simulator or on the device using Development mode) it stops your app, and immediately tells you what’s gone wrong.
When you do a Release build (by default: anything you send out as Ad-Hoc, or to the Apple App Store), Apple automatically replaces your NSAssert calls with blank lines – nothing happens. Otherwise your app would crash in many situations where it’s probably OK to keep running.
But that means you lose this info that could be very valuable in discovering if your app has stopped working, and how/why – e.g. if Apple changed something in an iOS update that now breaks your app.
Ideally, we want to do something different for Release builds.
NSAssertionHandler to the rescue
In older platforms, you often had to manually do a #define to change the assert() function to something different, in different builds. That was midly annoying – complex macros are slightly harder to maintain than straight code – and not very configurable.
Fortunately, Apple uses an OOP approach via NSAssertionHandler. You can implement *multiple* subclasses of NSAssertionHandler, and switch between them at runtime.
First, create a subclass of NSAssertionHandler:
@implementation AssertionHandlerLogAll
-(void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...
{
NSLog(@"[%@] Assertion failure: FUNCTION = (%@) in file = (%@) lineNumber = %i", [self class], functionName, fileName, line );
}
-(void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...
{
NSLog(@"[%@] Assertion failure: METHOD = (%@) for object = (%@) in file = (%@) lineNumber = %i", [self class], NSStringFromSelector(selector), object, fileName, line );
}
@end
…then, in your AppDelegate class (whichever class implements “NSObject
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
NSLog(@"[%@] Setting a custom assertion handler", [self class] );
NSAssertionHandler* customAssertionHandler = [[AssertionHandlerLogAll alloc] init];
[[[NSThread currentThread] threadDictionary] setValue:customAssertionHandler forKey:NSAssertionHandlerKey];
...
// NB: your windowing code goes here - e.g. self.window.rootViewController = self.viewController;
}
Assertions – upload to Flurry, perhaps?
But why limit yourself to just one assertion handler?
If you’re using Flurry (or Google Analytics etc), why not automatically log + upload each assertion to Flurry, and have them show up in your Dashboard? That way, you can see if ANY assertions are firing – but also get a count of:
- how many
- which hardware (is this only happening on an iPhone 3GS, for instance?)
- which iOS version (perhaps it’s due to a bug in iOS v 4.2, but not in iOS 4.3?)
Something like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
/** start Flurry */
[FlurryAPI startSession:@"whatever your private Flurry Key is"];
NSLog(@"[%@] Setting a custom assertion handler", [self class] );
NSAssertionHandler* customAssertionHandler = [[AssertionHandlerSendToFlurry alloc] init];
[[[NSThread currentThread] threadDictionary] setValue:customAssertionHandler forKey:NSAssertionHandlerKey];
...
// NB: your windowing code goes here - e.g. self.window.rootViewController = self.viewController;
}