今の時点でこの情報が役に立つ人はあまりいない気もしますが、とりあえず。
ReactNativeの通常のプロセスでビルドをするとApplication bundleの直下にmain.jsbundle
がcopyされ、Imageなどから参照される画像はここを起点とした相対パスとして解釈されて読み込まれます。
別な場所に置いたjsbundleを使ってRCTRootViewを初期化した場合、ImageのsourceのURLはそのjsbundleが設置されている場所を起点とした相対パスとして解釈されるため、通常プロセスでAssetsを読み込んでくれるRCTXCAssetImageLoader
は読み込んでくれません。
そこで、ReactNativeのImageLoaderの仕組みに則って独自に以下の様なImageLoaderコンポーネントを作成することで、NSLibraryDirectory以下に配置されたjsbundleが、あたかもApplication Bundle直下に配置されたかのようにassets URLを書き換える事で、通常のビルドでbundleされたjsbundleであるかのように画像を読み込む事ができます。
#import <libkern/OSAtomic.h>
#import "RCTBridge.h"
#import "RCTUtils.h"
#import "../../node_modules/react-native/Libraries/Image/RCTImageLoader.h"
@interface LibraryAssetsImageLoader : NSObject <RCTImageURLLoader> @end
@implementation LibraryAssetsImageLoader
RCT_EXPORT_MODULE()
+ (NSString*)libraryDirectory
{
static NSString *dir = nil;
if(dir == nil){
dir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
NSUserDomainMask, YES) firstObject];
}
return dir;
}
- (BOOL)canLoadImageURL:(NSURL *)requestURL
{
if(!requestURL.isFileURL) return NO;
NSString *path = requestURL.path;
return [path hasPrefix:[LibraryAssetsImageLoader libraryDirectory]];
}
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
__block volatile uint32_t cancelled = 0;
dispatch_async(dispatch_get_main_queue(), ^{
if (cancelled) {
return;
}
// convert assets in libdir to bundled assets.
NSString *libdir = [LibraryAssetsImageLoader libraryDirectory];
NSString *path = imageURL.path;
path = [path substringFromIndex:libdir.length];
path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path];
NSString *imageName = RCTBundlePathForURL([NSURL fileURLWithPath:path]);
UIImage *image = [UIImage imageNamed:imageName];
if (image) {
if (progressHandler) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image named %@", imageName];
completionHandler(RCTErrorWithMessage(message), nil);
}
});
return ^{
OSAtomicOr32Barrier(1, &cancelled);
};
}
@end
これで何が嬉しいかというと、アプリのリリース後にjsbundleをダウンロードしてきて動的に更新できるようになるということですね。
Imageリソースをサーバーからダウンロードする形式にすれば問題なかったのですが、データ転送量とかを気にするとやっぱり最初からbundleしてある画像リソースはそのまま使いたいですしね。
動的にダウンロードしたjsをそのまま使っては危険ですので、jsbundle作成時に秘密鍵でサインして、アプリ側で公開鍵でvalidateするとかしましょう。