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.
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:
2) Add a target for the asset bundle.
3) Add the assets to the asset bundle.
4) Build the asset bundle.
5) Zip the asset bundle
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; }