Effective Method Swizzling in Swift

Posted on October 23, 2015 中文版

Update 11/16:This post and the example project have been updated to Swift 3 with the new dispatch_once syntax.

Get the sample project for this article from GitHub or zipped.

Method Swizzling is a well known practice in Objective-C and in other languages that support dynamic method dispatching.

Through swizzling, the implementation of a method can be replaced with a different one at runtime, by changing the mapping between a specific #selector(method) and the function that contains its implementation.

Swizzling diagram

While this seems extremely convenient, this functionality does not come without its drawbacks. Performing this sort of alterations at runtime, you can’t take advantage of all the safety checks that are usually available at compile time. Swizzling is something that should be used with care.

The definitive article on how to swizzle in Objective-C is available on NSHipster (and some additional details are here) and a comprehensive discussion on the perils of using method swizzling can be found on Stackoverflow.

Swift takes a static approach regarding method dispatching, but it’s still possible to perform method swizzling if some conditions are met.

Before giving you some pointers on how to use swizzling with Swift, let me reiterate that this technique should be used sparingly, only when a more “swifty” alternative to solve your problem does not exist and not considered as a real alternative to subclassing or to the use of protocols and extensions.

As described in another article on NSHipster, performing swizzling in Swift with a class from one of the base frameworks (Foundation, UIKit, etc…), except for a few gotchas, is not that different from what you were used to in Objective-C:


extension UIViewController {
    public override static func initialize() {

        // make sure this isn't a subclass
        if self !== UIViewController.self {
            return
        }

        struct Inner {
            static let i: () = {
                let originalSelector = #selector(UIViewController.viewWillAppear(_:))
                let swizzledSelector = #selector(UIViewController.newViewWillAppear(_:))

                let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector)
                let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)

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

                if didAddMethod {
                    class_replaceMethod(UIViewController.self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
                } else {
                    method_exchangeImplementations(originalMethod, swizzledMethod);
                }
            }()
        }
        let _ = Inner.i
    }

    // MARK: - Method Swizzling

    func newViewWillAppear(animated: Bool) {
        self.newViewWillAppear(animated)
        if let name = self.descriptiveName {
            print("viewWillAppear: \(name)")
        } else {
            print("viewWillAppear: \(self)")
        }
    }
}

In this example, additional operations needs to be performed for every UIViewController in the application but the original behavior of the viewWillAppear method needs to be preserved, this can be done only through swizzling.

The viewWillAppear method implementation will be replaced with the implementation of a new method named newViewWillAppear in initialize. Note that after the swizzling, what in the code seems to be a recursive call to newViewWillAppear will become a call to the original viewWillAppear method.

The first difference from the recommended Objective-C approach is that the swizzling is not performed in load.

The load method is guaranteed to be called when the definition of a class is loaded and this makes it the right place to perform method swizzling.

But load is a Objective-C only method and cannot be overridden in Swift, trying to do it anyway will result in a compile time error. The next best place to perform the swizzling is in initialize, a method called right before the first method of your class is invoked.

Enclosing all the operations that modify the methods in the lazy initialization block of a computed global constant ensures that the procedure will be performed only once (since initialization of these variables or constants uses dispatch_once behind the scenes).

And that’s what you need to know for classes from base frameworks or for bridged Objective-C classes. When instead, you plan to use pure Swift classes there are a few additional things you should keep in mind to be able to perform method swizzling correctly.

Method Swizzling with Swift classes

To use method swizzling with your Swift classes there are two requirements that you must comply with:

  • The class containing the methods to be swizzled must extend NSObject
  • The methods you want to swizzle must have the dynamic attribute

More information about why this is necessary can be found in Apple’s “Using Swift with Cocoa and Objective-C”:

Requiring Dynamic Dispatch

While the @objc attribute exposes your Swift API to the Objective-C runtime, it does not guarantee dynamic dispatch of a property, method, subscript, or initializer. The Swift compiler may still devirtualize or inline member access to optimize the performance of your code, bypassing the Objective-C runtime. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched. Because declarations marked with the dynamic modifier are dispatched using the Objective-C runtime, they’re implicitly marked with the @objc attribute.

Requiring dynamic dispatch is rarely necessary. However, you must use the dynamic modifier when you know that the implementation of an API is replaced at runtime. For example, you can use the method_exchangeImplementations function in the Objective-C runtime to swap out the implementation of a method while an app is running. If the Swift compiler inlined the implementation of the method or devirtualized access to it, the new implementation would not be used.

This also means that you can’t perform swizzling if the method that you want to replace has not been declared as dynamic.

Let’s see how this translates to code:


class TestSwizzling : NSObject {
    dynamic func methodOne()->Int{
        return 1
    }
}


extension TestSwizzling {
    
    //In Objective-C you'd perform the swizzling in load() , but this method is not permitted in Swift
    override class func initialize()
    {
        // Perform this one time only
        struct Inner {
            static let i: () = {
                let originalSelector = #selector(TestSwizzling.methodOne)
                let swizzledSelector = #selector(TestSwizzling.methodTwo)
                
                let originalMethod = class_getInstanceMethod(TestSwizzling.self, originalSelector)
                let swizzledMethod = class_getInstanceMethod(RestSwizzling.self, swizzledSelector)
                
                method_exchangeImplementations(originalMethod, swizzledMethod)
            }()
        }
        let _ = Inner.i
    }
    
    func methodTwo()->Int{
        // It will not be a recursive call anymore after the swizzling
        return methodTwo()+1
    }
}

var c = TestSwizzling()
print(c.methodOne())  //2
print(c.methodTwo())  //1

In this simplified example the implementations of methodOne and methodTwo will be replaced with one another, just before the first method of the TestSwizzling object is called.

Closing remarks

As you have seen, it’s still possible to perform method swizzling in Swift, but in my opinion, most of the times it should never end up in actual production code. What a quick fix using swizzling can solve, can be better solved (for various definition of better) refactoring your code and thinking a better architecture.

Did you like this article? Let me know on Twitter!

I'm also on Twitter and GitHub.

Subscribe via RSS or email.