April 13th, 2012

iOS Logging ++

While Android inherits Java’s logging conventions with android.util.Log, iOS developers usually end up using Cocoa’s basic NSLog statement. This turns Objective-C logging the equivalent of Java’s System.out.println().  The three biggest problems with this are:

  1. No indication of where the log statement was generated
  2. No easy way to turn statements on or off
  3. Only one log level

These and other shortcomings of Objective-C’s NSLog statement have been frequently discussed over the years, and several solutions have been offered. They usually involve a #define statement you stick in your xxx-Prefix.pch file, taking advantage of the C preprocessor macros described here (the stuff between the double underscores below) and looks something like this:

#define SomeLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);

This tells the preprocessor to replace SomeLog… with NSLog… before handing everything over to the compiler. You can also enclose the above line in an #ifdef DEBUG block to take care of the second shortcoming mentioned above, and  the third by defining additional levels and using more than one #define, such as SomeLogTrace(). This works, and is a great improvement.

There are a couple of major drawbacks, though. Developers now have to learn to use SomeLog() where they would normally use NSLog(). In practice, this is a harder habit to break than you’d imagine, especially for larger teams. Secondly, you’ll also need to replace all of the existing NSLog() statements if you have an existing codebase and want to take advantage of the more verbose output.

A better solution is to replace NSLog() outright, and add additional logging levels if you find them necessary or useful in your project (for the sake of simplicity, I usually do not). To do this, we’ll define a function that replaces NSLog(), and then use a #define to replace all of the NSLog() statements with our version. You’ll continue to use NSLog as usual, but see improved console output.

Create an empty file called “Common.m” in your ARC project, making sure to add it to your target(s), and paste the following:

void TLog(const char *file, int line, NSString *format, ...) {
   va_list args = NULL;
   NSString *logString = nil;

   va_start(args, format);
   logString = [[NSString alloc] initWithFormat:format arguments:args];

   va_end(args);

   NSDateFormatter *dateFormatter = [NSDateFormatter new];
   [dateFormatter setDateFormat:@"HH:mm:ss.SSS"];

   logString = [NSString stringWithFormat:@"%@ %s:%d %@",[dateFormatter stringFromDate:[NSDate date]], file, line, logString];

   puts([logString UTF8String]);
}

And in your xxx-Prefix.pch file, add the following:

void TLog(const char *file, int line, NSString *format, ...);
#define  NSLog(args...) TLog([[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__,args)
This is a similar approach adopted by Test Flight in their SDK, which incidentally is named TFLog. If you’ve done this correctly, you’ll end up with a log statement that looks something like this:
12:24:05.624 Jaws.m:42 thats some bad hat harry
If you’d like to tweak the log output, change the format string on line 13 to anything you’d like.