zero one zero dave wraights blog …

24Jun/120

Article: Startup Pack


Startup Pack


What will you learn?

This article documents one approach I use when setting up new iOS applications, the process saves me time and future frustration and I’d like to share it with you in the hope that it will save you time, frustration and maybe even make your life easier.

By the end of the article you’ll have learnt how to setup unit testing, add a logging framework and integrate a ratings library into your applications.

Introduction

As developers we all have at some point in time setup a project or application and added a bunch of libraries that make our lives easier.

We do it so we can reduce our data access code, make logging easy or even a bunch of UI libraries that we’ve written over the years, regardless of the type of pack, the reason is the same; we are lazy - and in this case that’s a good thing.

Because everyone’s toolset and process is different this is a guide for integrating a generic set of libraries and controls into a new project to make the developer and even independent software vendor’s life easier; basically to allow our laziness to continue.

I call it the startup pack since I generally add the same libraries and toolkits each time I start a new project or inherit an existing codebase.

For those wanting to ease the difficulty of using CoreData you might want to investigate MagicalRecord, I have used some of their helper methods throughout this article and I use them on a regular basis with my own applications.

The Usual Pack Members

My applications usually contain the same things: testing framework, custom UI control libraries, a ratings library a feedback and a logging systems.

Let’s walk through the setup of each of these in a single view application.

Creating the Sample Project

For the purposes of demonstrating the startup pack in an actual project, let’s create a sample single view application.

Steps
1. From the File menu choose New > 'Project …' (CMD+SHIFT+N).

2. Choose the 'Single View' application (iOS > Applications) (the components in this startup pack can be used in most project types).

3. Click Next

4. Enter a product name and your company identifier and click Next (Remember not to select the 'Include Unit Tests' checkbox if you intend to add the custom unit testing framework from this tutorial).

5. Optional:  Select the 'Create Git Repository'.

6. Save the Project.

Testing Framework

Whenever you create a project you can add the SenTest/OCTest unit testing framework to your project. In fact you can add this post project startup/creation, although it is a little harder.

I have used this framework in a few past projects however I wasn’t really happy with how it works. For example, OCUnit won’t allow me to run a single unit test (or class of tests), instead it forces me to run ALL of the unit tests every time. The thing that really bugged me is that there isn’t a separate test runner, something I have become very used to in other development environments such as Eclipse, Visual Studio and even Netbeans.

Having this separate runner gives me the ability to structure releases with specific tests, creating a future way to run unit tests for specific versions to find regressions / breaking feature changes.

The testing framework that I use almost every time is the GHUnit testing framework. It addresses the biggest issues I have with OCUnit (such as the test runner), plus its open source so if I need to add an assertion type or understand what the library is doing I simply peak under the head and check out the macros.

The following steps are needed in order to get GHUnit up and running; oh and I’m assuming that you didn’t check that little ‘Include Unit Tests’ setting when you created the project (both can exist in the same project, though it might get confusing).

Downloading The Framework

There are two things we need to download, firstly we need to download the most current packaged version of the framework (at the time of writing that was version 0.4.33GHUnitIOS-0.4.33.zip’) from here.

The second and required file is the GHUnit main class. I’m not sure why this isn’t included in the archive, but it’s a standard main class which you can create yourself or you can find it by browsing the source here and creating a file called GHUnitIOSTestMain.m (or similar). Copy/Save this file into your projects Test folder.

Unzip GHUnitIOS-0.4.33.zip and and copy the entire folder into your project’s Test folder. We’ll add it to the XCode project fully in just a moment.

Creating the Target

The way to separate the application from other output types for your project is to use a Target. In XCode a target is (simplistically talking) a collection of compiler directives and settings with the specific outcome of having some output from running a build.

The target could be a static library, the application or even a separate third party project that your project depends upon. GHUnit uses a separate test target to configure the dependencies etc and compiles into a separate library for your application.

Steps
1. From the Project Navigator (CMD+1), select your primary project.
2. At the bottom of the screen on the right is a large button with a plus sign inside a circle titled 'Add Target'.
3. Selection, Applications and choose 'Single View' application and click 'Next'.
4. Name the Testing application (by convention I add the word 'Testing' to the project name).
5. Click Finish.
6. From the Project Navigator right click the folder that was just created and choose 'Add Files to 'Test' …' ('Test' will be replaced with the name you chose).
7. Select the 'GHUnitIOS.framework' folder (single click).  (Important) UnCheck the project target and CHECK the Test applications target.
8. Click 'Add'.

We need to have a project main class (the GHUnitIOSTestMain.m) so let’s add this file in and ensure it belongs to the Test Target’s project membership.

Steps
1. Right click the 'Test' folder and select 'Add Files to 'Test' …'
2. Select the GHUnitIOSTestMain.m file and CHECK the 'Test' project target and UNCheck the applications project target membership.
3. Click Add.
4. Select the 'ViewController', 'AppDelegate' and 'main' files (.m/.h/.xib) from the Test folder and delete them (GHUnit has it's own).

There are two compiler linker flags needed in order to build the Test target successfully ‘-ObjC’ and ‘-all_load’, lets add them now:

Steps
1. From the Project Navigator, select the Project, and then choose the 'Test' target from the list on the right.
2. Click the 'Build Settings' section, scroll down to find 'Other Linker Flags', click the '+'/Add button and enter '-ObjC -all_load'.  This will be added to both Release and Debug configurations. (If you have more you will need to add it to those also).
SDK Version

Change the Deployment for the Test Project to 4.3, by:

1. From the Project Navigator, select the Project, and then choose the 'Test' target from the list on the right.
2. Click the 'Summary' section and change the Deployment target to '4.3'
ARC/NON-ARC

If your project is using ARC (Automatic Reference Counting), the GHUnitIOSTestMain. you get from the GitHub repository isn’t ARC compatible. You will need to perform a couple of extra steps either to make it ARC compatible or set the Target to be a non-ARC project.

Non-Arc:
1. From the Project Navigator, select the Project, and then choose the 'Test' target.
2. Click the 'Build Settings' section, scroll down to find 'Objective-C Automatic Reference Counting' and switch it to 'NO'.
ARC:
1. Open the GHUnitIOSTestMain.m file, replace it's contents with the following ARC friendly AutoRelease Pool

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate"));
    }
}

GHUnit should now be up and running, select it from the Scheme chooser and Build/Run.

Logging

Over the years I’ve found that certain tools can make a difference, either during the development of the project or after, once the application has gone live. An example of this is logging.

In the beginning I never cared much about logging; it seemed like just one of those things that took too much time to setup and the benefit seemed negligible.

It wasn’t until I had written an application that was intermittently crashing. What made it harder to resolve was the fact it was happening on iOS 4 and 5 and across 3GS, 4 and 4S. I began with the usual questions like “What were you doing when it …”, and “How many items did you have at the time…” and of course “Were you connected to the server at the time…” and my personal favourite “How many fingers did you have on the screen at the time…”.

More often than not this leads nowhere and your users can quickly loose faith in your ability to find and fix the problem.

In this instance it wasn’t until I actually saw the problem occur was I able to resolve it - almost two weeks AFTER the issue had been initially reported. Adding on the time to go back through the stores' approval process it was a 4 week turnaround. I wasn’t happy and neither was the customer.

What should I have done? Logging. If I had put a mechanism in place to trace and retrieve the logging information I would have been able to find and resolve the issue in under 20 minutes. When I start any new project now I always add a mechanism to centralise and filter log messages, and I sprinkle my code with logging messages as I develop.

The mechanism I have employed is one of replacing the NSLog statement with my own ZLog version. I’ve also backed in a couple of extra’s; firstly I have a ‘logging level’ parameter which can control what get’s logged and what is ignored (I like to call it controlling the noise) and secondly there’s optional support for writing to a CoreData store.

Setting Up the Logging Framework

The whole idea behind this logging mechanism is to make it simple to call and use; to this end we are going to create a macro that let’s us capture critical information at the right time and place.

We need to define a structure that contains the constants that will define the logging levels. I prefer to create a Constants class to store stuff like this. I also like to include the Constants file in my applications prefix file (*_Prefix.pch - pre-compiled headers) so the Constants are available everywhere in the application without having to include it everytime I start a new class or view controller.

Code - Constants.h
typedef enum
{
    ZDebugLevelNone = 0,
    ZDebugLevelDebug = 1,
    ZDebugLevelInfo = 1 << 1,
    ZDebugLevelWarn = 1 << 2,
    ZDebugLevelError = 1 << 3,
    ZDebugLevelCritical = 1 << 4,
    ZDebugLevelTrace = 1 << 5
}ZDebugLevel;

#define kLOG_TO_CORE_DATA 1
#define kAPPLICATION_DEFAULT_DEBUG_LEVEL 4 // WARN

These logging levels are designed to work from the MOST noise (Debug - 1) to the least noise (Critical - 16). By using a default debug level we can set a bar for what is logged or ignored. If TRACE is set as the kAPPLICATION_DEFAULT_DEBUG_LEVEL it will allow ALL log messages to be logged regardless of the log level parameter value.

I often use this approach to provide a global ‘Turn Tracing On’ technique in the event a customer has a problem they are experiencing and I need to see as much detail about the application as possible (or that I have added logging for).

Now let’s create a new class based on NSObject to hold the code:

ZLog.h
#ifdef DEBUG
    #define ZLog(logLevel,args...) _ZLog(__FILE__,__LINE__,__PRETTY_FUNCTION__,logLevel,args);
#else
    #define ZLog(logLevel,x...)
#endif

void _ZLog(const char *file, int lineNumber, const char *funcName, int logLevel, NSString *format,...);

In the header we first check if we are running in DEBUG configuration (another defined constant), we then create a definition for the ZLog method that points to the internal _ZLog method. When running in the Debug configuration we get the luxury of debug symbols which gives us the file name, line number and name of the function the log message was written in. In Release (or Archive) configuration we don’t have symbols so we define a macro that passes just the information we have.

You might be wondering how we can have a definition for a macro pointing to the same method but with different number of parameters. The trick is in the three dots ‘’; they tell the compiler the method is a variadic, in other words the method can take a variable number of arguments.

One important thing to note is that when we call the logging method we don’t need to terminate the call with a ‘nil’, the compiler does that for us.

We’ll look more closely at the implementation and how we make use of those variables.

ZLog.m
void _ZLog(const char *file, int lineNumber, const char *funcName, 
           int logLevel, NSString* format,...) 
{
    int applicationDebugLevel = kAPPLICATION_DEFAULT_DEBUG_LEVEL;

    // Dont log it if we dont need it, but do it all the time if Tracing is turned on.
    if(logLevel < applicationDebugLevel && 
       applicationDebugLevel != DebugLevelTrace) 
            return;

    va_list ap;
    va_start (ap, format);

    // Let's add the EOL if it's not there
    if (![format hasSuffix: @"\n"]) 
    {
        format = [format stringByAppendingString: @"\n"];
    }

    // NSString's initWithFormat is also a variadic method.
    NSString* body =  [[NSString alloc] initWithFormat: format arguments: ap];
    va_end (ap);

    const char* threadName = [[[NSThread currentThread] name] UTF8String];
    NSString* fileName = [[NSString stringWithUTF8String:file] lastPathComponent];

    // Write to the Console
    NSString* logString = nil;
    if (threadName) 
    {
        logString = [NSString stringWithFormat:@"%s/%s (%s:%d) %s",threadName,funcName,[fileName UTF8String],lineNumber,[body UTF8String]];
        fprintf(stderr,logString);
    }
    else
    {
        logString = [NSString stringWithFormat:@"%p/%s (%s:%d) %s",[NSThread currentThread],funcName,[fileName UTF8String],lineNumber,[body UTF8String]];

        fprintf(stderr,logString);
    }
    //[body release];   //UnComment for non-ARC apps

    if(kLOG_TO_CORE_DATA)
    {
        AppLog* appLog = [AppLog MR_createEntity];
        appLog.lastModifiedByUser = @"Dave"; // Make this dynamic based on your needs
        if(logString)
        {
            appLog.logMessage = [logString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        }
        else
            appLog.logMessage = @"";

        appLog.creationDate = [NSDate date];
        appLog.createdBy = @"Dave";
        appLog.logLevel = [NSNumber numberWithInt:logLevel];

        [[NSManagedObjectContext MR_defaultContext] MR_save];
    }
}

Much of the code is straightforward so I’ll cover the lines which might not be immediately clear:

  1. Check the passed in Debug level against the application debug level, if it’s lower then there’s no point continuing - unless Tracing is on.
  2. Next we extract the passed in (variable number of) arguments and create a string using the format string supplied and the list of arguments in the ‘…’. (See below for an explanation on how variadic variables are parsed)
  3. The next part formats the variables and prints it to the console (just like NSLog does), if there’s a thread it also extracts that information for displaying and logging.
  4. The last part I’m just checking if we are writing to the CoreData entity.

Other than the logging to CoreData the logging method doesn’t write to disk or return the string for further usage (such as writing to disk or sending via emails.) I’ll leave that to your imagination and modification.

Writing a Log Entry

After all of that we can now log errors, information or debug data to the console (and optionally CoreData). Anywhere in our code that we would like to write a log entry we can do the following:

ZLog(ZDebugLevelWarn,@"The user did something to cause this warning.");
Variadic Methods
va_list

This is a type of object for holding the list of arguments.

va_start

This macro knows the starting address of the list of parameters and puts it into the supplied va_list variable. The second parameter to the va_start function is the format used for the other arguments.

va_arg

Although we don’t use this macro (we pass the entire list to NSString’s own variadic method), it provides a way to iterate over each argument, each time it’s called it moves to the next argument.

va_end

Once we are done getting the arguments out from the va_list variable and creating the string from all of the arguments ([[NSString alloc] initWithFormat: format arguments:ap]) we need to set the variable defined by va_list to nil, va_end does this for us.

Zombies

Whilst we are on the subject of logging there’s an environment setting you can set that can make a big difference whilst you are developing. These are the ZombieEnabled and AutoreleaseFreedObjectCheckEnabled environment variables.

When an object is deallocated it is automatically turned into an _NSZombie and it’s memory not actually freed, anytime you then use the _NSZombie the application wont crash and instead it will give you a chance to set a breakpoint on subsequent runs and also print a message to the console.

Now the big gotcha here is that this feature is great whilst you are developing, but it’s a pretty good idea to turn it off when you go live.s

Turning it on:
1. Choose ‘Edit Scheme’ from the scheme chooser
2. Select the ‘Run’ step, and click the ‘Arguments’ section
3. Click the ‘Add’ button under the ‘Environment Variables’ area.
4. Type NSZombieEnabled, with a value of ‘YES’
5. Click the ‘Add’ button again
6. Type ‘AutoreleaseFreedObjectCheckEnabled’ with a value of YES
5. Click OK

One more thing. We need to give ourselves a reminder that Zombie catching is turned on.

  1. Open up the AppDelegate for your application
  2. As one of the first lines in the ‘application:didFinishLaunchingWithOptions’ method add the following:

    if(getenv("NSZombieEnabled") || getenv("NSAutoreleaseFreedObjectCheckEnabled"))
    NSLog(@"NSZombieEnabled/NSAutoreleaseFreedObjectCheckEnabled enabled!");

Ratings

The AppStore is based on ratings. Like it or not, the more ratings your apps have the greater the chance they will have to be seen by potential new customers.
There are a lot of ways to get customers to rate your apps, but what if they forget to rate your application? A little reminder would help right?

Enter Appirater. This third party library (available here) integrates seamlessly into your application and prompts your users to rate your application.

You configure Appirater by modifying the supplied header file Appirater.h, with your application’s name and Application ID. After a pre-set (but configurable) number of startups, Appirater automatically prompts the user to rate your application on the AppStore by popping up an Alert View. There are three buttons in the alert ‘Rate’, ‘Remind’ and ‘Cancel’. If the user chooses to rate your application they are automatically taken to the AppStore page to rate your app.

Steps
  1. Download (or clone) the toolkit from here (git clone http://github.com/arashpayan/appirater/).
  2. Copy/Add the Appirater.m/.h files into your application’s project.
  3. Open your AppDelegate.h file and add the import for the Appirater.h to the top (#import “Appirater.h”).
  4. In the application:didFinishLaunchingWithOptions method right before the ‘return YES’, add the following call to tell Appirater your application has started up.

    [Appirater appLaunched]; return YES;

Next we need to configure Appirater:

Steps
  1. Open the Appirater.h file
  2. Change the value assigned to the APPIRATER_APP_ID define to your own applications ID.

Note: The name that Appirater shows comes from your bundle display name as set in your targets' Information.

One last thing about testing, in order to test the Appirater is working and going to the correct URL when you tap the ‘Rate’ button you don’t have to wait - simply edit the Appirater.h file and change the APPIRATER_DEBUG define to a YES.

Feedback

I believe that if you genuinely want to make your products better you will need your customers feedback. There are lots of ways to capture feedback from users, but by far the simplest way I have found (but not the only mechanism I use) is to provide a button somewhere inside the user interface to allow users to give you feedback.

There are two simple reasons I use email:

  1. I get their email address so I can (if they permit me) have a conversation after the initial email to understand the need or problem they are experiencing and;
  2. I find automated systems can sometimes restrict and constrain the customers' response - they can become far too automated in their quest to gather information from the user.

An added benefit of email is that the user can see what information is being sent to us. Bear in mind that this might not be a great solution if the information being sent is sensitive, in those cases I would build an encryption system to allow me to encrypt the data in way that I can get it out on the other side (server or my local development machine).

Often it’s the little things people say in emails that lead to good ideas, or better understandings of why they have started the conversation.

I take a four tiered approach with my feedback email system:

  1. Personalised Email. I create a personalised email containing a ‘Feedback/Comments’ section and usually a section for ‘What I was doing when the issue occurred’ and a statement explaining that any information they share is confidential and that I will not share their email address with anyone - ever (which I wont); sometimes I will ask if email is the preferred medium for getting back to them.
  2. Screenshot. The feedback system takes a screenshot of what was on screen prior to the Feedback button being pressed. (In some apps this might not be safe due to privacy reasons)
  3. System Logs. I bundle up the system logs. I create a file (optionally encoded using something like Base64) of any relevant system logs I have in Core Data. In some cases I prompt and ask permission prior to attaching the logs.
  4. Device Metrics. I insert into the body of the message (so the user can see what I’m sending) a list of Key/Value pair information regarding the device, it’s OS version, screen resolution, application version/build and so on.

Let’s walk through each part.

Personalised Email

In order to send email’s we need to include the MessageUI.framework; once included we add the protocol to our class’s interface/header to allow us to receive messages from the MessageUI delegate methods defined in MFMailComposeViewControllerDelegate. This protocol’s method will allow us to respond to the outcome of attempting to send the email.

Steps
  1. From the Project Navigator select your Project and choose your application target.
  2. Select the Build Phases, and click the ‘+’ (Add) button to add the ‘MessageUI.framework’ library.
  3. Open the Header (.h) file for either an existing class or view controller OR create a new class.
  4. Add the import statement for the MessageUI.framework to the top of your class:

    #import <MessageUI/MessageUI.h>
  5. After the interface declaration add the protocol declaration:

    <MFMailComposeViewControllerDelegate>
  6. Go to your classes implementation (.m) and add the required protocol’s method:
Code - Protocol Implementation
 - (void) mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
    if(result == MFMailComposeResultCancelled)
    {
        // Let the user know it was canceled
    }
    else if(result == MFMailComposeResultSent)
    {
        // Let the user know the email was sent
    }
    else if(result == MFMailComposeResultFailed)
    {
        // Let the user know the email failed to send, even though a valid account was found
    }

    [controller dismissModalViewControllerAnimated:YES];
}

7 . Wire up a button or other element in your applications UI to trigger a method in the same class as the protocol implementation above (in my case I’ve called it generatedFeedbackEmail):

Email Generation
// As a rule this code would not reside inside the event handle for the button, but for shortness' sake it is.
- (void) generateFeedbackEmail:(id)sender // called by the UI Button etc
{

    if([MFMailComposeViewController canSendMail])
    {
        // Grab the entire screen as a UIImage and wrap that into NSData for sending
        // via email as an attachment
        UIImage* screenShot = [self generateScreenshot];
        NSData* imageData = UIImagePNGRepresentation(screenShot);

        MFMailComposeViewController *mailComposer = [[MFMailComposeViewController alloc] init];
        mailComposer.delegate = self;
        NSMutableString* body = [[NSMutableString alloc] init];

        NSString* dashes = @"<br/>------------------<br/>";
        NSString* CR = @"<br/>";

        // The feedback Section
        [body appendFormat:@"Section/Area/Function%@%@%@%@%@%@",CR,dashes,CR,CR,CR,dashes];
        [body appendFormat:@"Comments/Feedback/Suggestion%@%@%@%@%@%@",CR,dashes,CR,CR,CR,dashes];

        // The privacy statement
        [body appendFormat:@"%@%@Your email and data are considered private and will never be shared.%@",CR,dashes,CR];

        // Add the Log Data
        [body appendFormat:@"%@%@%@%@", dashes,CR,[self generateMetricsString],dashes]

        [mailComposer setMessageBody:body isHTML:YES];
        [mailComposer setToRecipients:[NSArray arrayWithObject:@"feedback@yourdomainname.com"]];
        [mailComposer setSubject:@"Application Feedback"];

        // Add the Image as an image in PNG format.
        [mailComposer addAttachmentData:imageData mimeType:@"image/png" fileName:@"screenshot.png"];

        // Add the Logs
        [mailComposer addAttachmentData:[self generateLogData] mimeType:@"text/plain" fileName:@"AppMetricsandLogs.csv"]; 

        [self presentModalViewController:mailComposer animated:YES];
        [mailComposer release];

    }
    else
    {
        // Let the user know there's no email account setup.
    }  
}
Supporting Methods

The above code constructs a Mail Composer form and set’s it properties, including the body content (set as HTML). The screenshot is also added as an attachment to the email along with any Logs you might have collected (again as an attachment).

Creating the Screenshot

Something that has helped me in the past several times has been a capture of what the user was doing prior to tapping the feedback button.

In order to grab the screen we need to create an empty graphics context and iterate over ALL of the controls on the screen rendering each control’s layer to the graphics context. Once we have a copy of each controls layer we can turn the graphics context into a UIImage and return it for use inside the email.

Note: This code was written by (and copyright of) Apple as per here.

Steps
  1. Add an import for the QuartzCore framework to the top of your implementation (Add it in your Target’s Library if not included already):
    #import <QuartzCore/QuartzCore.h>
    (This allows the manipulation of layers amongst other functions).
  2. Next add a method (and it’s declaration in the header) to be called from the email generation method:
Code - generateScreenshot
- (UIImage*) generateScreenshot 
{
    // Create a graphics context with the target size
    // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to take the scale into consideration
    // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
    CGSize imageSize = [[UIScreen mainScreen] bounds].size;
    if (NULL != UIGraphicsBeginImageContextWithOptions)
        UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
    else
        UIGraphicsBeginImageContext(imageSize);

    CGContextRef context = UIGraphicsGetCurrentContext();

    // Iterate over every window from back to front
    for (UIWindow *window in [[UIApplication sharedApplication] windows]) 
    {
        if (![window respondsToSelector:@selector(screen)] || 
             [window screen] == [UIScreen mainScreen])
        {
            // -renderInContext: renders in the coordinate space of the layer,
            // so we must first apply the layer's geometry to the graphics context
            CGContextSaveGState(context);
            // Center the context around the window's anchor point
            CGContextTranslateCTM(context, [window center].x, [window center].y);
            // Apply the window's transform about the anchor point
            CGContextConcatCTM(context, [window transform]);
            // Offset by the portion of the bounds left of and above the anchor point
            CGContextTranslateCTM(context,
                                  -[window bounds].size.width * [[window layer] anchorPoint].x,
                                  -[window bounds].size.height * [[window layer] anchorPoint].y);

            // Render the layer hierarchy to the current context
            [[window layer] renderInContext:context];

            // Restore the context
            CGContextRestoreGState(context);
        }
    }

    // Retrieve the screenshot image
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return image;
}
Adding the Logs and Metrics

We need several methods to retrieve the logs and device metrics information. Firstly we need to gather our log entries from CoreData (or other source), write them to a file and add the file to the outgoing email as an attachment, then we need to create a string containing the metrics you want to gather from the device (these can be anything you like or is pertinent to your application).

Code - cacheDirectoryFilePath
- (NSString*) cacheDirectoryFilePath:(NSString*) documentFileName
{
    NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
    NSString *docDir = [arrayPaths objectAtIndex:0];
    NSString *fileName = [docDir stringByAppendingString:[NSString stringWithFormat:@"/%@",documentFileName]];
    return fileName;

}
Code - generateConcatenatedLogString
- (NSString*) generateConcatenatedLogString
{
    NSMutableString* logString = [[NSMutableString alloc] init];
    [logString appendFormat:@"\"%@\",\"%@\",\"%@\",\"%@\",\"%@\"\n",
     @"Log Date",
     @"Log Level",
     @"Log Message",
     @"Log Thread",
     @"Log Method"];

    NSArray* zLogsFromCoreData = [ZLog MR_findAll]; // MagicalRecord helper method
    for(ZLog* zLog in zLogsFromCoreData)
    {
        [logString appendFormat:@"\"%@\",\"%@\",\"%@\",\"%@\",\"%@\"\n",
         zLog.logDate,
         zLog.level,
         zLog.message,
         zLog.thread,
         zLog.methodName];
    }

    return logString;
}
Code - generateMetricsString
// All pre-calculated strings are stored/defined in 'Constants.h/.m'
- (NSString*) generateMetricsString
{
    NSMutableString* metricsString = [[NSMutableString alloc] init];
    [metricsString appendFormat:@"Application Version:&nbsp;&nbsp;%@<br/>",[NSString stringWithFormat:@"%@ [%@]",[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"],[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]] ];

    [metricsString appendFormat:@"OS Versions:&nbsp;&nbsp;%f / %f / %f<br/>",kCFCoreFoundationVersionNumber, kCFCoreFoundationVersionNumber_iOS_4_2,kCFCoreFoundationVersionNumber_iPhoneOS_5_0];
    [metricsString appendFormat:@"Screen Height Portrait:&nbsp;&nbsp;%f<br/>",SCREEN_HEIGHT_PORTRAIT];
    [metricsString appendFormat:@"Screen Height Landscape:&nbsp;&nbsp;%f<br/>",SCREEN_HEIGHT_LANDSCAPE];
    [metricsString appendFormat:@"Screen Scale:&nbsp;&nbsp;%f<br/>",SCREEN_SCALE];
    [metricsString appendFormat:@"Screen Width Portrait:&nbsp;&nbsp;%f<br/>",SCREEN_WIDTH_PORTRAIT];
    [metricsString appendFormat:@"Screen Width Landscape:&nbsp;&nbsp;%f<br/>",SCREEN_WIDTH_LANDSCAPE];
    [metricsString appendFormat:@"System Name:&nbsp;&nbsp;%@<br/>",SYSTEM_NAME];
    [metricsString appendFormat:@"System Version:&nbsp;&nbsp;%i<br/>",SYSTEM_VERSION];
    [metricsString appendFormat:@"Device Model Name:&nbsp;&nbsp;%@<br/>",DEVICE_MODEL];
    [metricsString appendFormat:@"Device Name:&nbsp;&nbsp;%@<br/>",DEVICE_NAME];
    [metricsString appendFormat:@"Device Orientation:&nbsp;&nbsp;%i<br/>",DEVICE_ORIENTATION];
    [metricsString appendFormat:@"Device Type:&nbsp;&nbsp;%@<br/>",DEVICE_TYPE];
    [metricsString appendFormat:@"iPad:&nbsp;&nbsp;%i<br/>",IS_IPAD];
    [metricsString appendFormat:@"iPhone:&nbsp;&nbsp;%i<br/>",IS_IPHONE];

    return metricsString;
}
Code - generateLogData
- (NSData*) generateLogData
{
    NSString* logString = [self generateConcatenatedLogString];

    [logString writeToFile:[self cacheDirectoryFilePath:@"AppMetricsandLogs.csv"] atomically:YES encoding:NSUTF8StringEncoding error:nil];

    NSData* attachmentData = [NSData dataWithContentsOfFile:[self cacheDirectoryFilePath:@"AppMetricsandLogs.csv"]];

    return attachmentData;
}

These methods work together to supply the data for the ‘generateFeedbackEmail’ method and present the user with a single modal email window where all the user has to do is tap the ‘Send’ button.

Summary

In this article I discussed and documented several ideas and techniques for making your development life easier both during the development lifecycle and after once your application has been released to your users. I also explained a technique for ‘documenting’ your applications code when it is running on your users devices using a logging framework.

Dave Wraight

With over 15 years software engineering experience across many of the popular programming frameworks and platforms I’ve really come to enjoy the mobile application development space. In particular the design and development of iOS applications allows me the freedom to design from paper and create visually rich and highly interactive applications. As founder of Zero One Zero software (www.010.com.au / blog.010.com.au) and developer of several existing iOS/Web/Desktop applications I am available for hire both locally and for remote work.

My Apps

  1. Landing Soon, LandingSoonApp.com
  2. reFuelr (WA/Australian residents only), reFuelrApp.com

Favourite iOS applications

  1. TouchDocs, from KabukiVision / AppStore
  2. Dooo, from Figtree Labs
  3. Flipboard, from Flipboard
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

(required)

No trackbacks yet.