So, whether you're still in the development stages or your app is already on the app store, you always hope your app isn't crashing. But if it is, you want to be sure you've got good crash reports, right? Moreover, if your app is on the appstore, it may not be sufficient to wait around for Apple to upload crash reports that iOS automatically generates and submits (for users that have allowed this).
Plus, wouldn't it be nice if your crash reports came with a screenshot (when possible) of what the user saw at the instant of the crash?
This code strives to be able to accomplish some of these things.
First, the relevant parts of class categories:
UIImage+(Screenshot).m
@implementation UIImage (Screenshot)
+ (instancetype)screenshot {
UIWindow *window = [UIApplication sharedApplication].keyWindow;
CGSize windowSize = [window bounds].size;
UIGraphicsBeginImageContext(windowSize);
CGContextRef context = UIGraphicsGetCurrentContext();
[window.layer renderInContext:context];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenshot;
}
@end
NSDate+(NowString).m
@implementation NSDate (NowString)
+ (NSString *)nowString {
NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setDateFormat:@"YYYY-MM-dd HH:mm:ss zzz"];
return [df stringFromDate:[self date]];
}
@end
The categories exist merely for convenience, and I am interested in hearing any ways to improve them, however the main code under review is the code that actually handles unhandled exceptions and generates crash logs:
AppDelegate.m
#import "AppDelegate.h"
#import "CustomCategories.h"
static NSString * const kKEY_CRASH_REPORT = @"CRASH_REPORT";
static NSString * const kKEY_ExceptionName = @"UnhandledExceptionName";
static NSString * const kKEY_ExceptionReason = @"UnhandledExceptionReason";
static NSString * const kKEY_ExceptionUserInfo = @"UnhandledExceptionUserInfo";
static NSString * const kKEY_ExceptionCallStack = @"UnhandledExceptionCallStack";
static NSString * const kKEY_ExceptionScreenshot = @"UnhandledExceptionScreenshot";
static NSString * const kKEY_ExceptionTimestamp = @"UnhandledExceptionTimestamp";
__unused void unhandledExceptionHandler(NSException *exception) {
NSMutableDictionary *crashReport = [NSMutableDictionary dictionary];
crashReport[kKEY_ExceptionName] = exception.name;
crashReport[kKEY_ExceptionReason] = exception.reason;
crashReport[kKEY_ExceptionUserInfo] = exception.userInfo ?: [NSNull null].debugDescription;
crashReport[kKEY_ExceptionCallStack] = exception.callStackSymbols.debugDescription;
UIImage *screenshot = [UIImage screenshot];
if (screenshot) {
crashReport[kKEY_ExceptionScreenshot] = UIImagePNGRepresentation([UIImage screenshot]);
}
crashReport[kKEY_ExceptionTimestamp] = stringFromDate([NSDate date]);
[[NSUserDefaults standardUserDefaults] setObject:[NSDictionary dictionaryWithDictionary:crashReport] forKey:kKEY_CRASH_REPORT];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&unhandledExceptionHandler);
// NSArray *crashArray = @[@"Hello", @"World"];
//
// NSLog(@"This creates a crash! %@", crashArray[7]);
NSDictionary *crashReport = [[NSUserDefaults standardUserDefaults] objectForKey:kKEY_CRASH_REPORT];
if (crashReport) {
NSLog(@"%@", crashReport);
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kKEY_CRASH_REPORT];
}
return YES;
}
@end
The code in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
exists merely to demonstrate how this is used.
I think more likely, the constants and handler function will be moved into their own file so that I can just copy that file into any/all future projects.
Realistically, the code in the if
block here would upload the crash report to a server or ask the user if they want to email it to the developer, etc. Currently the code just exists in a demo project I'm using to developer the UE handler.
As a note, it's not acceptable to simply wrap
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
in a try-catch block for several reasons, the primary of which it will prevent the app from properly crashing (so the OS can generate its own crash report) without preventing the app from closing.
screenshot
method could easily be modified to only grab what my app is displaying (even if there was a notification on screen, it wouldn't be grabbed). \$\endgroup\$