Sunday, December 12, 2010

Global log level control with cocoalumberjack

I started playing around with cocoalumberjack on the iPad to record user interactions and usage with the DDFileLogger.   The need to have a global log level became necessary otherwise I would have to assign ddLogLevel in every class.  

static const int ddLogLevel = LOG_LEVEL_VERBOSE;

The cocoalumberjack instructions mentions this feature, but doesn't supply details on the implementation.

I am not 100% sure if this is correct way but it works well enough.

Create a class and create a global ddLogLevel constant and set your required logging level.

Constant.h
extern int const ddLogLevel;

Constant.m
#import "Constants.h"
#import "DDLog.h"

int const ddLogLevel = LOG_LEVEL_VERBOSE;

Configure your logger.

#import "DDLog.h"
#import "DDASLLogger.h"
#import "DDTTYLogger.h"
#import "DDFileLogger.h"

...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
 
 [DDLog addLogger:[DDASLLogger sharedInstance]];
 [DDLog addLogger:[DDTTYLogger sharedInstance]];
 
 DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; 
 [DDLog addLogger:fileLogger];
 [fileLogger release];

...


To use this now simple import your class.

#import "DDLog.h"
#import "Constants.h"

...

- (void)someMethod {
 DDLogVerbose(@"Log this message");
}

Sunday, December 5, 2010

iOS : Replace a SQLite database at runtime using Core Data

I have been developing an application that required the SQLite database file to be replaced at runtime. This event was triggered when a new database file was downloaded from a remote server. I considered forcing the user to close the application, however this would be a poor user experience and just poor form.

One major issue was releasing the existing database correctly and then re-connecting.  The last hurdle was to clear any existing references to any managed objects currently being viewed or cached.

After searching the net and some experimenting I finally have something that is working.

- (BOOL) updateDB {
 
 if (managedObjectContext != nil) {
  [managedObjectContext lock];
  [managedObjectContext reset];
 }
 
 if (persistentStore != nil) {
  NSError *error;
  if (![persistentStoreCoordinator removePersistentStore:persistentStore error:&error]) {
      NSLog(@"Unable to remove persistent store error %@, %@", error, [error userInfo]);
      return FALSE;     
  }
 }
 if (managedObjectContext != nil) { 
     [managedObjectContext unlock];
 }
 if (persistentStore != nil) {
     [persistentStoreCoordinator release], persistentStoreCoordinator = nil;
 }
 if (managedObjectContext != nil) {
     [managedObjectContext release], managedObjectContext = nil;
 }
 if (managedObjectModel != nil) {
     [managedObjectModel release], managedObjectModel = nil;
 }
 
 [self replaceDB];
 
 [self managedObjectContext];
 
 [self clearDBReferences];
 
 return TRUE;
}

The managedObjectContext is initialised lazily, therefore is could be nil.

- (NSManagedObjectContext *) managedObjectContext {
    
    if (managedObjectContext != nil) {
        return managedObjectContext;
    }
  
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator: coordinator];
    }
    return managedObjectContext;
}

The order of the messages are important to ensure the resources are released in the reverse order they were initialised.

The [self replaceDB] simple copies the new SQLite DB into place.

The [self managedObjectContext] simple forces the Lazily initialisation.

The last message races though all controllers requesting them to clear any references to managed objects.

I simple created a ReleaseResource protocol that has a clear method. If a controller implements this protocol then it can simple release any managed objects.

For example, I have a controller that subclasses UITableViewController.

-(void)clear {
    self.fetchedResultsController = nil;
    [self.tableView reloadData];
}

This code will obviously change depending on your application layout. This method races through the controllers looking for classes that implement the ReleaseResource protocol.

- (void) clearDBReferences {
    for (UIViewController *controller in tabBarController.viewControllers) {
  // for iPad
  if ([controller isKindOfClass:[UISplitViewController class]]) {
   NSLog(@"Clearing iPAD DB References");
   for (UINavigationController *navController in ((UISplitViewController *) controller).viewControllers) {
    [navController popToRootViewControllerAnimated:FALSE];
    for (UINavigationController *rootController in ((UINavigationController *) navController).viewControllers) {
     if ([rootController conformsToProtocol:@protocol(ReleaseResources)]) {
      [rootController performSelector:@selector(clear)];
     }
    }
   }
  }  
  // for iPhone
  if ([controller isKindOfClass:[UINavigationController class]]) {
   UINavigationController *navController = ((UINavigationController *) controller);
   NSLog(@"Clearing iPhone DB References");   
   [navController popToRootViewControllerAnimated:FALSE];
   for (UINavigationController *rootController in ((UINavigationController *) navController).viewControllers) {
    if ([rootController conformsToProtocol:@protocol(ReleaseResources)]) {
     [rootController performSelector:@selector(clear)];
    }
   }
  }
 }
}