RT Blog
Programming

Method Swizzling

When anyone mentions swizzling I automatically get worried. Method swizzling is an Objective C runtime feature which allows for the switching of method implementations. This means any function can theoretically be switched out for another one at run time. The Objective C runtime has a whole host of runtime features to modify classes, methods, objects, properties etc. We’re going to look at method swizzling, which as mentioned before, is the process of changing a function implementation for another one at runtime.


So is Swizzling really a bad thing?

If not done correctly swizzling can open a whole host of issues.

Harder to debug – As we are swapping method implementations out, this can make debugging slightly harder. If you pick up a new project and don’t know a certain method has been swizzled, it can be quite hard to follow the implementation addresses in the backtrace.

Anyone can swizzle – As long as the class is an Objective C type its method can be swizzled. Anyone is free to change the code, as long as they know the class, and the name of the selector. This means any framework could modify parts of your app without you even knowing. It also means if you are swizzling a function, and another framework is swizzling the same function, there is going to be a mismatch, and only one implementation will be used.

Confusing – For anyone used to programming with Swift and using the Swift APIs, the Objective C runtime can be quite confusing, and hard to read. Having to get methods and implementations to swap around is not something that is done very often.


When to use Swizzling

The majority of the time I would say that swizzling should not be your first option, and that your problem could most likely be solved another way. However, there is one use case I’ve come across which I feel is a good candidate for method swizzling.

App Delegate Swizzling for Frameworks

Frameworks, be it 3rd party or local frameworks, have no knowledge of the app lifecycle. They do not get application events such as applicationWillEnterForeground, didFinishLaunchingWithOptions, handleEventsForBackgroundURLSession, etc. Frameworks do have access to the notification centre where they can subscribe to, and listen to changes to many of the application functions. However, notifications are not sent with all of them. For example, push notification configuration, and background fetching, do not send notifications when their respective functions have been called. So what happens when a framework needs to modify an app delegate function, but has no way to see the function?

Manually pass values in

The user could manually pass the values into the framework when the relevant app delegate method is fired. The example below shows an app delegate, and a framework class which would live inside a completely separate module.

import Framework

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    return true
  }
  
  func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
      // Pass through the values to our framework.
          Framework.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
  }
}
class Framework {
  
  static func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    // Handle the app delegate in our new framework.
  }
}

Although this way of engineering does solve the problem, it does mean that everyone who adopts the framework will have to manually pass these values through.

It would be better if the user didn’t have to do this extra step, and the framework just handled the logic on its own. This is where method swizzling comes in. By swizzling the app delegates handleEventsForBackgroundURLSession function, the framework can take care of this logic without the user having to do anything.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    return true
  }
  
  func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
      // No need to do anything now as the framework will 
      // take care of this.
  }
}

The framework will now swizzle the implementation of handleEventsForBackgroundURLSession from the app delegate to an implementation of a method from inside the framework.

class Framework {
  
  func loaded() {
      // Get a reference to the current app delegate.
    guard let delegate = UIApplication.shared.delegate else { return }
      // Get a reference to the selector of the app delegate method we want to change.
    let selector = #selector(UIApplicationDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:))
      // Create a new selector using the method inside the framework.
   let newSelector = #selector(application(_:handleEventsForBackgroundURLSession:completionHandler:))
      // Get the Method from the new selector and framework class.
    let swizzled = class_getInstanceMethod(Framework.self, newSelector)!
      // Get the Method of the original app delegate class. 
    if let originalMethod = class_getInstanceMethod(type(of: delegate), selector) {
        // If the original method exists we then swap the implementation to the new one in the framework.
      method_exchangeImplementations(originalMethod, swizzled)
    } else {
        // If the selector does not exist we add a method the app delegate which uses the implementation in the Framework class.
      class_addMethod(type(of: delegate), selector, method_getImplementation(swizzled), nil)
    }
  }
  
    // The swizzled function which will now be called instead of the app delegate version.
  @objc func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    print("swizzled")
  }
}

A few things to note here.

First, it creates the selectors by getting a reference to the app delegate, and a newly created function within the Framework. It then checks the app delegate to see if handleEventsForBackgroundURLSession: is already visible. If the user has already implemented handleEventsForBackgroundURLSession: then it has an implementation to swap, otherwise it will add the method to the app delegate using class_addMethod. Let us take a look at the two key swizzling functions in more detail.

method_exchangeImplementations(originalMethod, swizzled)

This switches the implementation of two Method types. These Method types are pulled from the selectors created at the beginning, along with their respective class types.

class_addMethod(type(of: delegate), selector, method_getImplementation(swizzled), nil)

If the method implementation does not exist within the app delegate, then it will have nothing to swizzle. So in this instance, a new method is added to the app delegate, using the function above. It creates a new method from a selector and the implementation of handleEventsForBackgroundURLSession: which lives inside the framework.

Now whenever handleEventsForBackgroundURLSession: is called in the app delegate, it will always call the function inside the framework. This is all well and good but what happens when the user still wants to provide their own functionality, or another framework wants to access handleEventsForBackgroundURLSession.

For this case, I would always provide a way for the user to disable swizzling. This way they can manually go back to importing the values without swizzling. This seems to be the adopted way of handling swizzling as is used in many 3rd party frameworks, Firebase, Airship etc.

Related posts

I don’t want this Class!

Rowan
6 years ago

Property Wrappers

Rowan
5 years ago

Running Xcode Tests from CI

Rowan
5 years ago
Exit mobile version