0

I am looking at the Swift code of the ThemeKit theming library.

In particular I would like to understand the following code in NSColor+ThemeKit.swift:

// ThemeKit.set() replacement to use theme-aware color
@objc public func themeKitSet() {
    // call original .set() function
    themeKitSet()

    // check if the user provides an alternative color
    if ThemeManager.shared.isEnabled && isThemeOverriden {
        // call ThemeColor.set() function
        ThemeColor.color(with: Selector(colorNameComponent)).set()
    }
}

There is what appears to be an endless recursive call, but presumably can't be, since the code works fine. This is confirmed by setting a breakpoint on the call to themeKitSet(). It is not possible to step into the call and execution continues without recursion.

Earlier in the file there is the following call:

swizzleInstanceMethod(cls: NSClassFromString("NSDynamicSystemColor"), selector: #selector(set), withSelector: #selector(themeKitSet))

With the implementation in NSObject+ThemeKit.swift as follows:

/// Swizzle instance methods.
@objc internal class func swizzleInstanceMethod(cls: AnyClass?, selector originalSelector: Selector, withSelector swizzledSelector: Selector) {
    guard cls != nil else {
        print("Unable to swizzle \(originalSelector): dynamic system color override will not be available.")
        return
    }

    // methods
    let originalMethod = class_getInstanceMethod(cls, originalSelector)
    let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)

    // add new method
    let didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))

    // switch implementations
    if didAddMethod {
        class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
    } else {
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
}

I suspect this is responsible for the magic, but my limited understanding of both Swift and Objective-C is letting me down.

What is happening here? Why is the apparently recursive call not actually recursive?

fractor
  • 1,534
  • 2
  • 15
  • 30
  • It calls the *original* implementation, see https://stackoverflow.com/q/13294420/1187415. – Martin R Nov 30 '19 at 14:43
  • 1
    This is why I never liked method swizzling. Even if you had no other choice but to change method implementations at runtime, I think it's way better to do so by binding to new selectors. If these methods were called `modifiedThemeKitSet` (whose IMPL was set as the IMPL for `themKitSet`), and `originalThemeKitSet` (whose IMPL is the original IMPL for `themKitSet`), then none of this confusion would have happened. – Alexander Nov 30 '19 at 14:50

1 Answers1

1

You correctly identified the magic bit: it's called method swizzling, and it's a way of wholesale replacing an existing method implementation.

You'll see this seemingly-recursive pattern a lot when method swizzling: that themeKitSet call actually runs the original implementation, as the comment says. It's because swizzling swaps the implementations of two methods, in this case themeKitSet and NSDynamicSystemColor.set.

Therefore, post-swizzle, NSDynamicSystemColor.set runs the code you see there, and themeKitSet has become the original implementation.

andyvn22
  • 14,696
  • 1
  • 52
  • 74