iosobjective-cswiftswift4swizzling

How do I swizzle main bundle with test bundle


I use to swizzle main bundle with test bundle like follow in obj c

#import "NSBundle+Bundle.h"
#import <objc/runtime.h>

@implementation NSBundle (Bundle)

+(void)loadSwizzler {
    static dispatch_once_t once_token;
    dispatch_once(&once_token,  ^{
        Method originalMethod = class_getClassMethod(self, @selector(mainBundle));
        Method extendedMethod = class_getClassMethod(self, @selector(bundleForTestTarget));
        //swizzling mainBundle method with our own custom method
        method_exchangeImplementations(originalMethod, extendedMethod);
    });
}

//method for returning app Test target
+(NSBundle *)bundleForTestTarget {
    NSBundle * bundle = [NSBundle bundleWithIdentifier:@"Philips.AppInfraTests"];

    return bundle;
}

@end

But I tried the following for the same in swift

     extension Bundle {
  class func swizzle() {
        let originalSelector = #selector(mainBundle)
        let swizzledSelector = #selector(testBundle)
        let originalMethod = class_getInstanceMethod(self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }

    func mainBundle() -> Bundle
    {
        return Bundle.main
    }
    func testBundle() -> Bundle
    {
        return Bundle(for: self.classNamed("swizzler")!)
    }
}

But this is throwing some errors "Argument of '#selector' cannot refer to variable 'testBundle'"

could some one help me how do I do it


Solution

  • This answer has been tested in Swift 3 & 4 Playground, any other version and YMMV.

    Your Objective-C code swizzles two class methods, your Swift version attempts to swizzle two instance methods - so they are not doing the same thing.

    You (probably) cannot swizzle a (pure) Swift function, you can swizzle Objective-C methods, this is due to the differences in how functions/methods are dispatched. So in Swift the replacement function must be marked @objc in Swift 4 (it is optional and apparently harmless in Swift 3).

    Swift renames mainBundle to main and surfaces it as a property, so to get the selector for mainBundle you need to use getter: main.

    Combine the above and you get the following Playground code:

    extension Bundle
    {
        class func swizzle()
        {
            let originalSelector = #selector(getter: main)
            let swizzledSelector = #selector(testBundle)
            let originalMethod = class_getClassMethod(self, originalSelector)!
            let swizzledMethod = class_getClassMethod(self, swizzledSelector)!
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    
        @objc class func testBundle() -> Bundle
        {
            // just for testing in Playground
            return Bundle(path: "/Applications/TextEdit.app")!
        }
    }
    
    let b = Bundle.main
    print(b)
    Bundle.swizzle()
    let c = Bundle.main
    print(c)
    

    which prints:

    NSBundle </Applications/Xcode.app> (not yet loaded)
    NSBundle </Applications/TextEdit.app> (not yet loaded)
    

    Note that class_getClassMethod() returns a Method? and the above code forces this without any checks, those checks should exist in real code!

    Finally note that your swizzle code assumes mainBundle is implemented directly by NSBundle and not one of its ancestors, that is probably a safe assumption in this case but is not always. See for example this question on doing swizzling safely.

    HTH