如何使用 NSBundle 从下载的包中加载 NIB 并回退到主包

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

这是基本问题:我需要一种视图加载机制,尝试从文档中下载的 NIB 创建视图,如果无法创建视图,则回退到主包。

我经过大量研究和反复试验,才让它发挥作用,所以我想与其他人分享解决方案。

步骤如下:

1) 以正常方式在主包中创建 NIB。我建议使用指向文件夹的文件组,以将所有将用于下载包的资产放在一起。我们称它为 NIB_Resources.

要在项目导航器中的文件夹下创建 NIB:

  1. 右键单击文件组。
  2. 选择新文件…
  3. 选择用户界面,然后select查看。

2) 为资产包添加一个目标。

  1. 单击目标面板中的 +。
  2. Select Framework and Library 类别中的 Bundle 模板,在 OS X 下。它属于该类别,因为它是一种资产库。
  3. 对于产品名称,输入您要调用资产库的名称。保留其他所有内容,select 您要将产品添加到的项目。
  4. 在新产品的构建设置中,将 Base SDK 从 Latest OS X 更改为 Latest iOS。

3) 将资产添加到资产包中。

  1. Select 新产品的复制捆绑资源构建阶段。
  2. 拖放要包含在捆绑包中的资产。如果可以添加资产,光标将显示一个 + 图标。

4) 构建资产包。

  1. Select 新创建目标的方案。
  2. Select iOS 设备作为构建目标。
  3. 建造。
  4. 如果此操作正确,新捆绑包的产品应该在项目导航器的“产品”文件夹下从红色变为黑色。

5) 压缩资源包

  1. 在Products文件夹中右键点击新建的产品,select在Finder中显示
  2. 将包复制到某个位置,例如专用于此项目的目录中的某个文件夹。
  3. 右键单击包含捆绑包和其他可能的 NIB 文件、图像等的目录
  4. Select压缩。

6) 将资产包上传到您可以下载的位置。

7) 下载压缩资源包:

下面的代码隐藏在便利函数中,在处理大量低级文件系统操作的便利文件中。 FS 前缀指的是文件系统。

FSDownloadTempFileWithURLString 可以在返回到主线程之前从辅助线程调用。

我使用NSData同步方法,initWithContentsOfURL:,因为调用很可能是从辅助线程进行的。基本策略是将 zip 文件下载到一个临时位置(Caches 目录通常是一个很好的选择),然后再进行任何必要的准备并将文件解压缩到 Documents 目录。在头文件中定义内联静态操作的方法是从 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) 使用 SSZipArchive 或类似的东西将下载的文件解压到 Documents 目录:

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

9) 最后,尝试从 Documents 目录中的捆绑包中的 NIB 文件加载视图,然后回退到主捆绑包。

可以从尝试从 Nib 加载视图的视图控制器调用下面的 FSResourceNib 操作,如下所示:

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;
}