primers

In iOS development, we package the SDK for use by third parties usually in the form of.A or.framework +.bundle. Those of you who have packaged the SDK with bundle resources will run into one of these problems. The code to load the resources in the custom Bundle is different from the code to load the resources in the mainBundle when we’re developing our App, which is a little bit more difficult to write.

If you are encapsulating an SDK with resources, I believe BundleLoader should help you. It eliminates this call difference by simply calling the two methods and loading resources from a custom Bundle “seamlessly” just like loading resources from an App. Existing code doesn’t need to be modified, and subsequent code can be developed in the simplest and most familiar way.

Project address: BundleLoader

The problem

Recently, I encountered such a need. I am a live broadcast APP, and my boss required me to separate the parts related to the live broadcast room from the APP and encapsulate them into SDK for the third party to use. In the future, SDK and APP should be developed and updated synchronically.

In this case, the difference in calls is a big problem for me. First, the broadcast room and the relevant part of the code is very large, all kinds of resources in various forms of call, change up is very troublesome. Second, after the change in the future synchronous development is also a trouble.

To solve this problem, let’s first look at how the code looks different. For example, we know that loading the image code in the main package of the App only requires a simple sentence:

UIImage *img = [UIImage imageNamed:@"pic"];
Copy the code

Loading images from a custom Bundle is a bit trickier:

NSString *path = [[NSBundle mainBundle] pathForResource:@"myBundle" ofType:@"bundle"];
NSBundle *bundle = [NSBundle bundleWithPath:path];
NSString *file = [bundle pathForResource:@"pic" ofType:@"png"];
UIImage *img = [UIImage imageWithContentsOfFile:file];
Copy the code

Or to simplify a little bit:

NSString *file2 = [[NSBundle mainBundle] pathForResource:@"myBundle.bundle/pic" ofType:@"png"];
UIImage *img2 = [UIImage imageWithContentsOfFile:file2];
Copy the code

To simplify a bit:

UIImage *img3 = [UIImage imageNamed:@"myBundle.bundle/pic"];
Copy the code

But it’s still not as simple as mainBundle. So, I thought, can I load resources in a custom Bundle without changing the code? There must be a way, OC strong Runtime out of the horse, there is no unmanageable things, ha ha.

features

The BundleLoader Demo currently tests seamless loading of custom bundle resources in the following cases:

  • The picture
  • xib
  • storyboard
  • Xcssets pictures
  • Common resource file

Xib or storyboard images and XCSsets will also work. Demo also provides a simple Framework + Bundle project template for your reference.

Other resources such as CoreData models, localized strings, etc. should be able to be loaded as well, but if not you can do it yourself.

implementation

The specific implementation is not complicated, but the most important point is: I found that no matter what type of resource is loaded in App and what interface is called, the system will call this method of NSBundle internally:

- (nullable NSString *)pathForResource:(nullable NSString *)name ofType:(nullable NSString *)ext;
Copy the code

This method is the breakthrough, as long as we work on this method, using flexible and powerful Runtime, should be able to achieve our goal.

The implementation steps are as follows:

  • Gets the object of the custom resource Bundle
  • Associate this object with the mainBundle object
  • Set the Class of mainBundle to the Class of the custom Bundle subclass
  • Rewrite it in a Bundle subclasspathForResource:ofType:methods
  • This method gets the associated custom Bundle object
  • Check whether the file exists in the custom Bundle object and return its path if it does
  • If it doesn’t exist, go to the mainBundle

The code:

@implementation BundleLoader

+ (void)initFrameworkBundle:(NSString*)bundleName {
    refCount++;
    NSBundle* bundle = objc_getAssociatedObject(self, NSBundleMainBundleKey);
    if(bundle = = nil) {/ / to get custom resource bundle object nsstrings * path = [[NSBundle mainBundle] pathForResource: bundleName ofType: @"bundle"]; NSBundle *resBundle = [NSBundle bundleWithPath:path]; Objc_setAssociatedObject ([NSBundle mainBundle], NSBundleMainBundleKey, resBundle, OBJC_ASSOCIATION_RETAIN_NONATOMIC); Object_setClass ([NSBundle mainBundle], [FrameworkBundle Class]); }}Copy the code
@interface FrameworkBundle : NSBundle @end@implementation FrameworkBundle - (nullable NSString *)pathForResource:(nullable NSString *)name ofType:(nullable NSString *)ext {NSBundle* bundle = objc_getAssociatedObject(self, NSBundleMainBundleKey);if (bundle) {
        NSString *path = [bundle pathForResource:name ofType:ext];
        if (path)
            return path;
    }
    return [super pathForResource:name ofType:ext];
}
Copy the code

Run the code and find that [UIImage imageNamed:@”crown”] is ready to get the UIImage object. I thought I could call it a day, but I was too happy. If the image is in xcassets, this call will still fail. The xcassets method of a custom Bundle can only be loaded using the following methods:

[UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];
Copy the code

Keep it up, this time the Method Swizzling. If you’re not familiar with this dark magic, look here. We gave the UImage’s imageNamed: Method Swizzling. The code is as follows:

@implementation UIImage (FrameworkBundle)

#pragma mark - Method swizzling

+ (void)load {
    Method originalMethod = class_getClassMethod([self class], @selector(imageNamed:));
    Method customMethod = class_getClassMethod([self class], @selector(imageNamedCustom:));
    
    //Swizzle methods
    method_exchangeImplementations(originalMethod, customMethod);
}

+ (nullable UIImage *)imageNamedCustom:(NSString *)name {
    //Call original methods
    UIImage *image = [UIImage imageNamedCustom:name];
    if(image ! = nil)return image;
    
    NSBundle* bundle = objc_getAssociatedObject([NSBundle mainBundle], NSBundleMainBundleKey);
    if (bundle)
        return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil]; // This is the only way to load images of xcassets in the bundleelse
        return nil;
}

@end
Copy the code

Call imageNamed: get the image, return it if you get it; Failure is called imageNamed: inBundle: compatibleWithTraitCollection: method to obtain images, and the incoming custom Bundle object. This also makes it easy to load xcassets images in the Bundle.

Same thing for XIBs and storyboards.

conclusion

The implementation is relatively simple, using three Runtime methods, respectively:

  1. associationsobjc_setAssociatedObject
  2. Changing object typesobject_setClass
  3. Method Swizzling method_exchangeImplementations

Use custom subclasses and custom methods to get the system to load files from our resource Bundle and then load them from the main Bundle.

If this library is useful to you, please give a thumbs-up, thank you.