10
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ If you do this, be sure to include in your privacy policy, and preferably up-front to users who won't read the privacy policy, the information that you may be taking a screenshot of their device and their data. I definitely would not want any of my apps (no matter how harmless) taking screenshots of my device and sending them to someone else. There could be personal notifications displayed, in the future multiple apps may be able to be opened at the same time on the same screen, and you could be collecting personal/private/NDA'd data from other apps if you do this. \$\endgroup\$ – user1118321 Jul 18 '14 at 2:37
  • \$\begingroup\$ In this particular code, I'm just grabbing the window. The 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\$ – nhgrif Jul 18 '14 at 2:41
  • \$\begingroup\$ Regardless, I wouldn't want Microsoft seeing what's in my Excel Spreadsheet window or my Word document window without my express permission. (And I do often give companies permission to see things like crash logs if they ask.) \$\endgroup\$ – user1118321 Jul 18 '14 at 5:20
8
\$\begingroup\$
+ (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;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.