When I was building Groceryx, I wanted it to be as solid and robust as possible. I've invested time in unit tests and did a thorough beta testing process and setup HockeyApp for crash reporting. However, I'm a realist and understand that bugs will be found anyway even if you try hard to avoid them. It's just the part of building apps, get along with it.

When a user finds a bug I want to have best possible information on what happened, when and where. Although user provided details are critical in most cases, it's not enough to pinpoint the source of the problem let alone fix it. I found that app's log file was an excellent source for all the info and details needed for the fix.

Of course, to have a log, you need to write it first. I assume that you already have some logging in your app, and you probably are using NSLog(). Since it writes to ASL you need to redirect it to a file (here is an example). I prefer to use CocoaLumberjack library with a simple text file logger setup. As a bonus, you get fast async file writing and log rotation (you don't want to fill up user space with old logs).

To have meaningful logs you need to cover most of your code with most edge cases you can think of. Warning: don't write any personal data or other sensitive information to your log. If you are using assertions in your code here is the macro I use. It resolves into NSAssert() in debug build and into logging with running a block of code in release build:

 #define AssertTrueOrRunBlock(condition, block, description, ...)\
 if (1) {\
     NSAssert((condition), (description), ##__VA_ARGS__);\
     if (!(condition)) {\
         DDLogError(@"Assertion failure! Condition not satisfied: %s, reason: '" description "'", #condition, ##__VA_ARGS__);\

Here is a sample usage:

AssertTrueOrRunBlock(list, {return nil;}, @"List cannot be nil");

In debug build it'll be an assertion and in release build it'll be log message with return nil; called after it.

For better understanding where in your code was any particular log message was generated you need to include source file name and line number along with function name. I use custom log formatter for this. Here is my logging setup:

@interface LineNumberLogFormatter : NSObject <DDLogFormatter>
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;

@implementation LineNumberLogFormatter
NSDateFormatter *_dateFormatter;

- (instancetype)init {
    if ((self = [super init])) {
        _dateFormatter = [[NSDateFormatter alloc] init];
        [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style
        [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
    return self;

- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
    NSString *logLevel;
    switch (logMessage->_flag) {
        case DDLogFlagError    :
            logLevel = @"E";
        case DDLogFlagWarning  :
            logLevel = @"W";
        case DDLogFlagInfo     :
            logLevel = @"I";
        case DDLogFlagDebug    :
            logLevel = @"D";
        default                :
            logLevel = @"V";
    NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)];
    return [NSString stringWithFormat:@"%@  %@  %@ (%@:%d):  %@", logLevel, dateAndTime, logMessage->_function, logMessage->_fileName, logMessage->_line, logMessage->_message];

NSData *PSLLogData(void) {
    NSMutableData *logData = [[NSMutableData alloc] init];
    for (id logger in [DDLog allLoggers]) {
        if ([logger isKindOfClass: DDFileLogger.class]) {
            DDFileLogger *fileLogger = logger;
            for (DDLogFileInfo *logFileInfo in [fileLogger.logFileManager sortedLogFileInfos]) {
                NSData *fileData = [NSData dataWithContentsOfFile:logFileInfo.filePath];
                if (fileData) {
                    [logData appendData:fileData];
    return logData.length > 0 ? logData : nil;

void PSLSetupLogging(void) {
    DDTTYLogger *ttyLogger = [DDTTYLogger sharedInstance];
    [ttyLogger setLogFormatter:[[LineNumberLogFormatter alloc] init]];
    [DDLog addLogger:ttyLogger];
    DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
    fileLogger.rollingFrequency = 60 * 60; // 1 hour rolling
    fileLogger.logFileManager.maximumNumberOfLogFiles = 2;
    [DDLog addLogger:fileLogger];
#ifdef DEBUG
    [DDLog addLogger:[DDASLLogger sharedInstance]];

Now we have our log in a text file, and we need a way for the user to send it to us. I've placed a "Send Feedback" item in app's "Settings" menu. It creates and opens an email with filled recipient's address and a log file attached (generated by PSLLogData()). I've also added a note that log file is attached to this email to let the user know.

This setup saved me tons of hours of figuring out the source of the problems. I urge you to spend some time and add something similar to your app.