swifttestingswizzling

NSLocale using method swizzling to change the currentLocale output for testing purposes


I'm trying to change the device currentLocale output to perform some interesting unit tests, this is the code that I'm using but it seems that the returning currentLocale doesn't get overridden. Any hint?

extension NSLocale {
    class func frLocale()->NSLocale{
        return NSLocale(localeIdentifier: "fr_FR")
    }

    class func forceCurrentLocale(){
        let originalSelector = #selector(NSLocale.currentLocale)
        let swizzledSelector = #selector(self.frLocale)

        let originalMethod = class_getClassMethod(self, originalSelector)
        let swizzledMethod = class_getClassMethod(self, swizzledSelector)

        let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
}

// EDIT

The code above doesn't work. But If I write it like this it works:

class func forceCurrentLocale(){
    let originalSelector = #selector(NSLocale.currentLocale)
    let swizzledSelector = #selector(NSLocale.frLocale)

    let originalMethod = class_getClassMethod(self, originalSelector)
    let swizzledMethod = class_getClassMethod(self, swizzledSelector)

    method_exchangeImplementations(originalMethod, swizzledMethod)
}

what's wrong with class_addMethod in that case?


Solution

  • Your first method would be correct to swizzle an instance method, but not for a class method. What happens – if I understand it correctly – is that

    let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
    

    adds an instance method to the class, and returns true. Then

     class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
    

    replaces an instance method, which fails.

    If you look at the Method Swizzling article from NSHipster then you'll find the following comment:

    // When swizzling a class method, use the following:
    // Class class = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(class, originalSelector);
    // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    

    Translated to Swift that would be

    class func forceCurrentLocale(){
        let originalSelector = #selector(NSLocale.currentLocale)
        let swizzledSelector = #selector(self.frLocale)
    
        let classObject : AnyClass = object_getClass(self)
    
        let originalMethod = class_getClassMethod(classObject, originalSelector)
        let swizzledMethod = class_getClassMethod(classObject, swizzledSelector)
    
        let didAddMethod = class_addMethod(classObject, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
    
        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
    

    and then the swizzling works as expected. (The crucial point is that class_addMethod() is called on the class object, not on self.)

    But actually I don't see any advantage over your second method. didAddMethod will always return false because frLocale is already defined as a class method of NSLocale.