Search code examples
iosuiimagensbundle

is it possible to share images across NSBundles?


Apologies for the vague question title...

here is my scenario:

I am dynamically loading UIView content from nib files stored in NSBundles

BOOL bundlePathExists = [fm fileExistsAtPath:bundlePath];
if (bundlePathExists) {
    NSBundle *tempBundle = [NSBundle bundleWithPath:bundlePath];

    UIViewController *vc = [[UIViewController alloc] initWithNibName:nibName bundle:tempBundle];

    [self.view addSubview:vc.view];
}

(note: the above is a simplified excerpt of the actual code, as not to confuse the nature of the question - the mechanics of doing this is relatively well documented here)

these nib files contain a number of images, some of which are unique to that bundle, and others that are shared across multiple bundles.

i'd like to be able to store the images that are common inside the main bundle, so they don't take up space in the other bundles, and to minimise the maintenance of the project as a whole - eg if i change one of those common images, i'd rather not have to rebuild every bundle it is referenced by.

i realize i could do this programatically by using

[commonImageViewIvar setImage:[UIImage imageWithName:commonImageName]]; 

however i would prefer to achieve this without writing custom code for each view (ie the view controller instantiated is not customised per nib, hence all information needs to be stored in the nib)


Solution

  • As promised here is the solution i came up with:

    The bundle files themselves are downloaded as described here. To add to the information listed there, I found that if you create the views in an iphone app first, you can then preview them in the simulator. then, you can create a new project, as a bundle, and drag and drop ALL the image files & the xib &.h file into the new project. don't drag the .m file across as this creates build issues. then, ensure that the project settings define the BASE SDK as "Latest IOS", and not the default Mac OS X setting that would have been selected. you can then still edit the xib by double clicking on it. any common files can be deselected from the build by unticking the "target" column for that file. see "Additional Information" later in this answer post.

    once i have downloaded each bundle in a zip file from the server, i utilize the following methods i coded for this purpose:

    -(NSArray *) imageFileExtensions {
        return [NSArray arrayWithObjects:@".png",@".gif",@".jpg",@".jpeg",@".bmp",  nil];
    }
    

    and

    -(void) cloneImageFilesInPath:(NSString *)path toPath:(NSString*)clonePath {
    
        NSFileManager *fm = [NSFileManager defaultManager];
        NSArray *extensions = [self imageFileExtensions];
        if ([fm fileExistsAtPath:path]) {
            NSArray *files = [fm contentsOfDirectoryAtPath:path error:nil];
    
            for (NSString *file in files){
                for (NSString *ext in extensions) {
                    if ([file hasSuffix:ext]) {
                        NSString *targetFile = [clonePath stringByAppendingPathComponent:file];
                        if (![fm fileExistsAtPath:targetFile]) {
                            [fm createSymbolicLinkAtPath:targetFile withDestinationPath:[path  stringByAppendingPathComponent:file] error:nil];
                        }
                        break;
                    }
                }
    
            }
        }
    
    }
    

    these methods create scan the main app bundle directory and for each image file that is NOT in the custom bundle directory, a symbolic link is created inside the custom bundle directory to point back to the main app bundle directory.

    They are invoked in my app delegate as follows:

            ...(insert code to unzip the file to bundlePath here)...
    
            [self cloneImageFilesInPath:[[NSBundle mainBundle] bundlePath] toPath:bundlePath];
    

    Additional information

    to create the actual bundles, i made a separate app that removed any image files from a custom bundle directory, if those filenames are present in a directory that contains the common image files that are deployed in the main app bundle. i later discovered you can prevent xcode from including certain files from the build by deselecting the target column checkbox for that file, so this step is not necessarily needed - if you have a lot of views to create however, it may be easier to just leave them in the bundles, and strip them out using this method.

    -(void) removeDuplicatedImageFilesInPath:(NSString *)sourcePath fromTargetPath:(NSString*)path {
    
        NSFileManager *fm = [NSFileManager defaultManager];
        NSArray *extensions = [self imageFileExtensions];
        if ([fm fileExistsAtPath:path]) {
            NSArray *files = [fm contentsOfDirectoryAtPath:sourcePath error:nil];
    
            for (NSString *file in files){
                for (NSString *ext in extensions) {
                    if ([file hasSuffix:ext]) {
                        NSString *targetPath = [path stringByAppendingPathComponent:file];
                        if ([fm fileExistsAtPath:targetPath]) {
                            [fm removeItemAtPath:targetPath error:nil];
                        }
                        break;
                    }
                }
    
            }
        }
    
    }
    

    Further Information for dealing with the zip files

    I opted on using ObjectiveZip (following a suggestion by a poster here. To simplify the task, i wrapped this in the following 2 app delegate methods (one is used in the actual app, another in the the offline bundle creation app)

    in the main app

    -(void) unzipArchive:(NSString *)zipFileName toPath:(NSString *)path {
        NSFileManager *fm  = [NSFileManager defaultManager];
    
        ZipFile *unzipFile = [[ZipFile alloc] initWithFileName:zipFileName mode:ZipFileModeUnzip];
    
        NSArray *infos= [unzipFile listFileInZipInfos];
    
        [unzipFile goToFirstFileInZip];
        for (FileInZipInfo *info in infos) {
    
            ZipReadStream *read1= [unzipFile readCurrentFileInZip];
    
            NSMutableData *fileData = [[[NSMutableData alloc] initWithLength:info.length] autorelease];
            int bytesRead1 = [read1 readDataWithBuffer:fileData];
    
            if (bytesRead1 == info.length) {
                NSString *fileName = [path stringByAppendingPathComponent:info.name ];
                NSString *filePath = [fileName stringByDeletingLastPathComponent];
    
                [fm createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil];
    
    
                [fileData writeToFile:fileName atomically:YES];
    
            }
    
            [read1 finishedReading];        
    
            [unzipFile goToNextFileInZip];
    
        }
    
        [unzipFile close];
        [unzipFile release];
    
    
    }
    

    and in the custom bundle creation offline app.

    -(void) createArchive:(NSString *) zipFileName usingPath:(NSString *)path {
        NSArray *files = [self filesInDirectory:path];
        ZipFile *zipFile= [[ZipFile alloc] initWithFileName:zipFileName mode:ZipFileModeCreate];
    
        for (NSString *file in files) {
    
            NSString *fileNameForZip = [file substringFromIndex:path.length];
    
            ZipWriteStream *stream= [zipFile writeFileInZipWithName:fileNameForZip fileDate:[NSDate dateWithTimeIntervalSinceNow:-86400.0] compressionLevel:ZipCompressionLevelBest];
    
            [stream writeData:[NSData dataWithContentsOfFile:file]];
    
            [stream finishedWriting];
        }
        [zipFile close];
        [zipFile release];
    }
    

    note: the previous method relies on the following 2 methods, which create an NSArray containing the fully qualified path to all files in the given path (recursing into sub directories)

    -(void)loadFilesInDirectory:(NSString *)path intoArray:(NSMutableArray *)files {
        NSFileManager *fm  = [NSFileManager defaultManager];
    
        NSMutableArray *fileList = [NSMutableArray arrayWithArray:[fm contentsOfDirectoryAtPath:path error:nil]];
        for (NSString *file in fileList) {
            BOOL isDirectory = NO;
            NSString *filePath = [path stringByAppendingPathComponent:file];
            if ([fm fileExistsAtPath:filePath isDirectory:&isDirectory]) {
                if (isDirectory) {
                    [self loadFilesInDirectory:filePath intoArray:files];
                } else {
                    [files addObject:filePath];
                };          
    
            };
        }
    }
    
    
    -(NSArray *)filesInDirectory:(NSString *)path {
        NSMutableArray *files = [NSMutableArray array];
        [self loadFilesInDirectory:path intoArray:files];
        return files;
    }