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.
1 Answer 1
+ (instancetype)screenshot;
While it is good to use the instancetype
as your return type to allow for subclass, there are two problems using it here.
First, this is a class category, not a class, and the only way for a category to be included in a subclass by default is kind of hacky. You just import the file the category is in in the subclass's header.
But the bigger problem here is this:
// stuff
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
// stuff
return screenshot;
The method doesn't actually return a dynamically typed object. It returns a UIImage
object--every time. This method will never return anything other than a UIImage
, but through some hacky methods, you could get the IDE to lie and claim it will return SomeUIImageSubclass
. The end result though is that the returned object will ALWAYS be a UIImage
and never anything else. As such, the declared return type should be changed from instancetype
to UIImage
:
+ (UIImage *)screenshot;
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\$