Element 84 Logo

Metaprogramming in Objective-C

04.18.2011

For the past few months I’ve gotten the opportunity to develop in Ruby and experience first-hand the awesomeness that is Ruby on Rails. I’ve been keeping up with my normal iOS development and would be lying if I said RoR development hasn’t changed my Objective-C habits. Ruby/Rails development has drilled into my head ideas such as DRY (Don’t Repeat Yourself), CoC (Convention over Configuration), and the idea of meta programming. I’ve started to realize that Objective-C and the iOS SDK leads you into the direction of hard-coded repetition: I cringe every time I implement UITableViewDataSource methods ::I’m looking at you UITableViewCell:: and I’m tired of implementing the same logic in navigation-based apps.

Objective-C has some built in support for meta-programming with calls such as doesRespondToSelect:, valueForKey:, and makeObjectsPerformSelector:. I’ve recently come across some nice solutions for UITableViewDataSource methods in iOS Recipes: Tips and Tricks for Awesome iPhone and iPad Apps by Paul Warren and Matt Drance.

I decided I’d see how far I can push the idea of meta-programming in Objective-C and came up with a fun solution for the very common navigation pattern: List of Models View -> Details of Model View. The solution relies heavily on CoC and meta programming.

The solution makes use of a custom class, ModelListViewController, which implements three methods:

- (void)insertNewObject;
- (NSString *)modelNameForList;
- (void)presentViewControllerForObject:(NSManagedObject *)anObject newObject:(BOOL)isNew;

The implementation of each method is fairly short:

- (void)insertNewObject {
  // This example uses Core Data, but you could replace this code with any initialization code.
  NSManagedObject *anObject = [NSEntityDescription insertNewObjectForEntityForName:[self modelNameForList] inManagedObjectContext:_managedObjectContext];
  [self presentViewControllerForObject:anObject newObject:YES];
}

- (NSString *)modelNameForList {
  NSString *className = NSStringFromClass([self class]);
  NSRange suffixRange = [className rangeOfString:@"ListViewController"];

  NSString *modelName = nil;
  if (suffixRange.location != NSNotFound) {
    modelName = [className substringToIndex:suffixRange.location];
  }

  return modelName;
}

- (void)presentViewControllerForObject:(NSManagedObject *)anObject newObject:(BOOL)isNew {
  // Dynamically initialize the correct detail view controller for this model by appending "ViewController" to the end of the model name.
  NSString *viewControllerName = [NSString stringWithFormat:@"{f915c993dcd57782d8bbdb9f9291c5d4b3b1a7c844f782feb26c4f553a22f754}@ViewController", [self modelNameForList]];

  // NSClassFromString() allows us to perform this dynamic initialization.
  ModelViewController *managedObjectController = [[NSClassFromString(viewControllerName) alloc] init];
  managedObjectController.managedObject = anObject;
  managedObjectController.newObject = isNew;

  // Wrap in a navigation controller.
  UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:managedObjectController];

  if (isNew) {
    [self presentModalViewController:navController animated:YES];
  } else {
    [self.navigationController pushViewController:managedObjectController animated:YES];
  }

  // Clean up.
  [navController release];
  [managedObjectController release];
}

The method that makes magic happen, modelNameForList, looks at the class name and pulls out anything before “ListViewController”. This allows you to dynamically insert new Model objects (assuming Core Data usage – but would work for anything) or dynamically initialize and present detail view controllers. The presentViewControllerForObject:newObject: method expects detail view controllers to be named something like ModelViewController, where “Model” is replaced with the actual model name – Convention over Configuration FTW!

Using an example: Let’s imagine you have a model named Car and have added a subclass of ModelListViewController named CarListViewController, and a class called CarViewController. By simply subclassing ModelListViewController, you’ve already achieved normal navigation based behavior for adding a new Car or tapping to view details of an existing Car with 0 lines of code in any of the Car classes. Of course, this example assumes you’ve implemented some of UITableViewDelegate and DataSource methods in ModelListViewController, but those examples are for another post.

Paul Pilone

Senior Software Engineer