How to use NSBundle to load NIBs from downloaded bundle and fall back to main bundle

424 views Asked by At

Here is the basic problem: I needed a view loading mechanism that attempts to create a view from a downloaded NIB in Documents, then falls back to the main bundle, if the view could not be created.

I've gone through a lot of research and trial and error, before getting this to work, so I wanted to share the solution with others.

1

There are 1 answers

0
Sheamus On BEST ANSWER

Here are the steps:

1) Create NIBs in the main bundle in the normal fashion. I recommend using a file group that points to a folder, to keep all the assets together that will be be used for the downloaded bundle. Let’s call it NIB_Resources.

To create a NIB under a folder in the Project Navigator:

  1. Right-click on a file group.
  2. Choose New File…
  3. Choose User Interface, and select View.

2) Add a target for the asset bundle.

  1. Click on the + in the targets panel.
  2. Select the Bundle template in the Framework and Library category, under OS X. It is in that category because it is a type of asset library.
  3. For the product name, enter the name that you want to call the asset library. Leave everything else as is, and select the project that you want to add the product to.
  4. In the Build Settings for the new product, change the Base SDK from Latest OS X to Latest iOS.

3) Add the assets to the asset bundle.

  1. Select the Copy Bundle Resources Build Phase for the new product.
  2. Drag and drop the assets that you want to include in the bundle. The cursor will show a + icon if it is possible to add the asset.

4) Build the asset bundle.

  1. Select the Scheme for the newly created target.
  2. Select iOS Device as the build target.
  3. Build.
  4. If this was done correctly, the product for the new bundle should have changed from red to black, under the Products folder of the Project Navigator.

5) Zip the asset bundle

  1. Right-click the newly built product in the Products folder, and select Show in Finder.
  2. Copy the bundle to a location, such as some folder in a directory dedicated to this project.
  3. Right-click on the directory that contains the bundle and possibly other NIB files, images, etc.
  4. Select Compress.

6) Upload the asset bundle to a location that you have download access to.

7) Download the zipped asset bundle:

The code below is tucked away in convenience functions, in a convenience file that handles a lot of low-level File System operations. The FS prefix refers to File System.

FSDownloadTempFileWithURLString can be called from a secondary thread, before returning to the main thread.

I use the NSData synchronous method, initWithContentsOfURL:, because the call is likely to be made from a secondary thread. The basic strategy is to download the zip file to a temporary location (the Caches directory is usually a good candidate for this purpose), before doing any necessary preparations and unzipping the file to the Documents directory. The approach of defining inline static operations in a header file was adopted from Apple.

//Documents directory
#define FSDocumentsDirectory    [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]

//Caches directory
#define FSCachesDirectory       [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]

/**
 * Get the path to a file under the caches directory. The given filename can have
 * multiple file separators.
 */
inline static NSString* FSCachesPath(NSString *filename)
{
    return [FSCachesDirectory stringByAppendingPathComponent:filename];
}


/**
 * Download a file from a specified URL, and copy to the caches directory, to the same filename as the URL's filename.
 *
 * Returns the result.
 */
inline static BOOL FSDownloadTempFileWithURLString(NSString *urlString)
{
    NSData *data = getDataFromURL(urlString);
    if (!data) {
        //Error already logged
        return FALSE;
    }

    NSString *path = FSCachesDirectory;
    NSString *filename = [urlString lastPathComponent];
    path = [path stringByAppendingPathComponent:filename];

    NSError *error = nil;
    if (![data writeToFile:path options:NSDataWritingAtomic error:&error]) {
        NSLog(@"Error occurred while trying to write the file to: %@\n", path);
        NSLog(@"%@", error);

        return FALSE;
    }

    return TRUE;
}

/**
 * Get the data from a specified URL.
 */
inline static NSData* getDataFromURL(NSString *urlString)
{
    NSString *escapedUrlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

    NSURL *url = [NSURL URLWithString:escapedUrlString];
    NSData *data = [[NSData alloc] initWithContentsOfURL:url];
    if (!data) {
        debugLog(@"Could not download file: %@", escapedUrlString);
        return nil;
    }

    return data;
}

8) Unzip the download to the Documents directory, using SSZipArchive or something similar:

NSString *cachesPath = FSCachesPath(URL_RESOURCE_FILENAME);
if (![SSZipArchive unzipFileAtPath:cachesPath toDestination:FSDocumentsDirectory delegate:nil]) {
    return;
}

9) Finally, try to load a view from a NIB file in the bundle in the Documents directory, and fall back to the main bundle.

The FSResourceNib operation below can be called from a view controller that is trying to load a view from a Nib, like so:

UIView *view = FSResourceNib(ResourcesBundle, nibName, self);
/**
 * Get a NIB from the documents directory, otherwise fall back to the bundle.
 *
 * Returns nil, if an error occurs.
 */
inline static UIView* FSResourceNib(NSString *bundleFilename, NSString *nibName, id owner)
{
    UIView *resourceView = nil;

    //If bundld doesn't exist in the documents path, then use the main bundle
    NSString *resourcePath = FSDocumentsPath(bundleFilename);
    if ([[NSFileManager defaultManager] fileExistsAtPath:resourcePath]) {
        NSBundle *resourceBundle = [NSBundle bundleWithPath:resourcePath];

        @try {
            //Try to load the NIB from the given bundle
            resourceView = [[resourceBundle loadNibNamed:nibName owner:owner options:nil] lastObject];
        }
        @catch (NSException *exception) {
            //do nothing - will try main bundle
        }
    }

    //If loading from the given bundle failed, try loading from the main bundle
    if (!resourceView) {
        NSBundle *resourceBundle = [NSBundle mainBundle];

        @try {
            resourceView = [[resourceBundle loadNibNamed:nibName owner:owner options:nil] lastObject];
        }
        @catch (NSException *exception) {
            //do nothing - will return nil, indicating an error occurred
        }
    }

    return resourceView;
}