Swizzling with Swift

Helder Pinhal
Jul 24 2020
Posted in Engineering & Technology

Swizzling your AppDelegate in a Framework

In this article we're going to talk about Swizzling. While there are various approaches to the term, our focus here is on ISA Swizzling.

This technique modifies a property — the ISA ('is a') — on a given object, which describes the class of the object. This allows us to switch the type of an object with another type at runtime.

While method swizzling deals with exchanging method implementations, possibly chaining their invocations, at runtime, that's only useful if we know the concrete implementation of UIApplicationDelegate and then make an extension on that type to implement the method swizzling. For this article we want to start on the principle we do not know the concrete type, which would be the case if our project is a framework.

In a multi-app scenario, sticking to the DRY principle tends to become even more important so let's assume we are swizzling from a Framework.

Before we actually do any swizzling, we need to prepare the type we are going to swizzle to. For that, let's start by creating a dynamic sub-class of the UIApplicationDelegate implementation and lastly perform the ISA swizzle.

private static func createSubClass(from originalDelegate: UIApplicationDelegate) -> AnyClass? {
    let originalClass = type(of: originalDelegate)
    let newClassName = "\(originalClass)_\(UUID().uuidString)"

    guard NSClassFromString(newClassName) == nil else {
        NSLog("Cannot create subclass. Subclass already exists.")
        return nil
    }

    guard let subClass = objc_allocateClassPair(originalClass, newClassName, 0) else {
        NSLog("Cannot create subclass. Subclass already exists.")
        return nil
    }

    self.createMethodImplementations(in: subClass, withOriginalDelegate: originalDelegate)
    self.overrideDescription(in: subClass)

    // Store the original class
    objc_setAssociatedObject(originalDelegate, &AssociatedObjectKeys.originalClass, originalClass, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

    guard class_getInstanceSize(originalClass) == class_getInstanceSize(subClass) else {
        NSLog("Cannot create subclass. Original class' and subclass' sizes do not match.")
        return nil
    }

    // Register our sub-class
    objc_registerClassPair(subClass)

    // Perform the ISA swizzle
    if object_setClass(originalDelegate, subClass) != nil {
        NSLog("Successfully created proxy.")
    }

    return subClass
}

The key point to perform the swizzling is the runtime function object_setClass which will exchange the type of the original object our the sub-class we dynamically created.

One thing to note is the name of the sub-class is something like AppDelegate_371CCFB4-BDD4-44B5-BF2D-17359F79B9FC. This is not a problem but should we log the AppDelegate, that's the kind of name that'll show up. We can improve this by keeping the description of the original AppDelegate.

private static func overrideDescription() {
    // Override the description so the custom class name will not show up.
    self.addInstanceMethod(
            toClass: subClass,
            toSelector: #selector(description),
            fromClass: AppDelegateSwizzler.self,
            fromSelector: #selector(originalDescription))
}

@objc
private func originalDescription() -> String {
    let originalClass: AnyClass = objc_getAssociatedObject(self, &AssociatedObjectKeys.originalClass) as! AnyClass

    let originalClassName = NSStringFromClass(originalClass)
    let pointerHex = String(format: "%p", unsafeBitCast(self, to: Int.self))

    return "<\(originalClassName): \(pointerHex)>"
}

Now let's add the createMethodImplementations method that's missing and actually have some methods (applicationDidBecomeActive: & applicationWillResignActive:) in our sub-class. This principle can be extended to whatever methods you may need.

private static func createMethodImplementations(
        in subClass: AnyClass,
        withOriginalDelegate originalDelegate: UIApplicationDelegate
) {
    let originalClass = type(of: originalDelegate)
    var originalImplementationsStore: [String: NSValue] = [:]

    // For applicationDidBecomeActive:
    let applicationDidBecomeActiveSelector = #selector(applicationDidBecomeActive(_:))
    self.proxyInstanceMethod(
            toClass: subClass,
            withSelector: applicationDidBecomeActiveSelector,
            fromClass: AppDelegateSwizzler.self,
            fromSelector: applicationDidBecomeActiveSelector,
            withOriginalClass: originalClass,
            storeOriginalImplementationInto: &originalImplementationsStore)

    // For applicationWillResignActive:
    let applicationWillResignActiveSelector = #selector(applicationWillResignActive(_:))
    self.proxyInstanceMethod(
            toClass: subClass,
            withSelector: applicationWillResignActiveSelector,
            fromClass: AppDelegateSwizzler.self,
            fromSelector: applicationWillResignActiveSelector,
            withOriginalClass: originalClass,
            storeOriginalImplementationInto: &originalImplementationsStore)

    // Store original implementations
    objc_setAssociatedObject(originalDelegate, &AssociatedObjectKeys.originalImplementations, originalImplementationsStore, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

@objc
private func applicationDidBecomeActive(_ application: UIApplication) {
    NSLog("Framework: application did become active")

    let methodSelector = #selector(applicationDidBecomeActive)
    guard let pointer = AppDelegateSwizzler.originalMethodImplementation(for: methodSelector, object: self),
          let pointerValue = pointer.pointerValue else {

        return
    }

    let originalImplementation = unsafeBitCast(pointerValue, to: ApplicationDidBecomeActive.self)
    originalImplementation(self, methodSelector, application)
}

@objc
private func applicationWillResignActive(_ application: UIApplication) {
    NSLog("Framework: application will resign active")

    let methodSelector = #selector(applicationWillResignActive)
    guard let pointer = AppDelegateSwizzler.originalMethodImplementation(for: methodSelector, object: self),
          let pointerValue = pointer.pointerValue else {

        return
    }

    let originalImplementation = unsafeBitCast(pointerValue, to: ApplicationWillResignActive.self)
    originalImplementation(self, methodSelector, application)
}
Our custom delegate methods, aside from performing their own logic — a simple log statement in this case — also try to find out if there was an implementation of it in the original AppDelegate and run the original code.

As you may have noticed, the examples above use some methods that are not shown in the code snippets. However, the whole AppDelegateSwizzler is available as a gist.

There you can find a complete example that ensures the code runs only once by leveraging Swift's lazy evaluation of a static property. Although the syntax might not be as clear as we would like, this is the recommended alternative to the dispatch_once function which is no longer available.

Furthermore, it includes the public setup method needed to trigger the whole swizzling process, reassigning the AppDelegate singleton as well as the missing methods to dynamically add methods to the sub-class.

Conclusion

In our opinion this approach is preferable to the surrogacy principle due to the fact that this way we keep all the custom props/methods of the original AppDelegate, preventing missing selector errors caused by replacing the AppDelegate singleton with a totally different surrogate.

As always, we are available via our Support Channel if you have any questions.

Keep up-to-date with the latest news