loading NSBundle files on iOS

Wolfgang Schreurs picture Wolfgang Schreurs · Jun 1, 2011 · Viewed 17.4k times · Source

I'd like to create a project that has a very flexible graphical user interface (skinnable). In order to make this possible, I'd like to load a NSBundle from an external resource, e.g. a website. The bundle should contain nibs that correspond to some properties and methods in the main project (IBOutlets & IBActions)

It seems Apple has limited the possibility to use NSBundle in such a way. Is there any way I can make this work? If it's not possible in the conventional way, what would be a recommendable alternate approach?

Answer

Wolfgang Schreurs picture Wolfgang Schreurs · Jun 3, 2011

As promised, here a short explanation on how I made it work.

In my AppDelegate I check if there's a new bundle (packaged in a zip file) on the server. If it exists, I download the bundle. The bundle contains the nibs I need for my skinnable graphical interface and other related data (e.g. graphic files). The code looks a bit like this:

TestAppDelegate.m

- (void)downloadBundle 
{      
   NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/~wsc/template.bundle.zip"];
   NSURLRequest *request = [NSURLRequest requestWithURL:url];
   NSURLResponse *response = nil;
   NSError *error = nil;
   NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
   NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
   NSLog(@"%d", [httpResponse statusCode]);

   if ([httpResponse statusCode] == 404) // bundle will be deleted and the default interface will be used ...
   {
      NSString *path = [documentsDirectory stringByAppendingPathComponent:@"template.bundle"];
      [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
      return;
   }
   else if (error) 
   {
      NSLog(@"%@", error);
   }

   BOOL didWriteData = [data writeToFile:zipFile atomically:YES];
   if (didWriteData) 
   {
      BOOL success = [SSZipArchive unzipFileAtPath:zipFile toDestination:documentsDirectory];
      if (!success) 
      {
         NSLog(@"failed to unzip file.");
      }
   }
}

Please note I make use of the SSZipArchive class as suggested by neoneye. It's required to package the bundle in some sort of container to download all contents efficiently, since a bundle is just a directory & file structure following Apple's conventions.

One of my classes is a ViewController that has a label and button as IBOutlets. The most important code in the ViewController looks like this:

TestViewController.h

@interface TestViewController : UIViewController {
   UIButton *button;
   UILabel *label;
}

@property (nonatomic, retain) IBOutlet UIButton *button;
@property (nonatomic, retain) IBOutlet UILabel *label;

- (IBAction)buttonTouched:(id)sender;

@end

TestViewController.m

@implementation TestViewController
@synthesize button, label;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

// the -init method is overridden to use nib file from bundle, if bundle exists ...
- (id)init 
{
   NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
   NSString *documentsDirectory = [paths objectAtIndex:0];
   NSString *file = [documentsDirectory stringByAppendingPathComponent:@"template.bundle"];
   NSBundle *bundle = [NSBundle bundleWithPath:file];
   if (!bundle)
   {
      NSLog(@"no bundle found, falling back to default gui ...");
      return [self initWithNibName:nil bundle:nil];
   }

   NSString *nibName = NSStringFromClass([self class]);
   return [self initWithNibName:nibName bundle:bundle];
}

- (void)dealloc
{
   [button release];
   [label release];
   [view release];

   [super dealloc];
}

#pragma mark - View lifecycle

- (void)loadView
{
   if (self.nibName && self.nibBundle) 
   {
      // connect outlets to proxy objects ...
      NSDictionary *objects = [NSDictionary dictionaryWithObjectsAndKeys:
          self.label, @"label", 
          self.button, @"button", 
          nil];
      NSDictionary *proxies = [NSDictionary dictionaryWithObject:objects forKey:UINibExternalObjects];
      NSArray *nibs = [self.nibBundle loadNibNamed:self.nibName owner:self options:proxies]; // connection happens here ...

      NSLog(@"nibs found with name %@: %d", self.nibName, [nibs count]);
      return;
   }

   // show default gui if no nib was found ...

   CGRect frame = [UIScreen mainScreen].applicationFrame;
   self.view = [[[UIView alloc] initWithFrame:frame] autorelease];
   [self.view setBackgroundColor:[UIColor lightGrayColor]];

   self.button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
   [self.button setFrame:CGRectMake(0.0f, 0.0f, 60.0f, 30.0f)];
   [self.button setCenter:CGPointMake(160.0f, 100.0f)];
   [self.button addTarget:self action:@selector(buttonTouched:) forControlEvents:UIControlEventTouchUpInside];
   [self.view addSubview:self.button];

   self.label = [[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 300.0f, 30.0f)] autorelease];
   [self.label setCenter:CGPointMake(160.0f, 50.0f)];
   [self.label setTextAlignment:UITextAlignmentCenter];
   [self.view addSubview:self.label];
}

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
   [super viewDidLoad];

   // for my purposes I'll add the localized string from the mainBundle here for the standard controls, 
   // this will override the text set in the nibs, since the nibs are loaded and displayed at this point ...
   [self.button setTitle:NSLocalizedString(@"TestButton", nil) forState:UIControlStateNormal];
   [self.label setText:NSLocalizedString(@"TestLabel", nil)];

}

- (void)viewDidUnload
{
   [super viewDidUnload];
   self.button = nil;
   self.label = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark -

- (IBAction)buttonTouched:(id)sender 
{
   [[[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"DialogTitle", nil) 
                                message:NSLocalizedString(@"ButtonTouchedText", nil) 
                               delegate:nil 
                      cancelButtonTitle:@"OK" 
                      otherButtonTitles:nil] 
     autorelease] show];
}

@end

The actual nib is defined in a seperate linked project (Cocoa NSBundle project with some parameters changed to make it work for iOS devices). The file's owner is TestViewController, so I can access all outlets & actions and make the appropriate connections. Please note there's no view (UIView *) property defined in TestViewController, since the superclass already has a view property. Make sure the view is connected in Interface Builder. Also note the nib file makes use of the same name as the actual class for ease of use.

Couldn't find much information on the web on how to do this, so I hope this will be helpful to lots of people that have similar goals.