SwizzleHelper

main

Swift package for swizzling Objective-C methods in Swift, and for attaching associated values to Objective-C classes and their instances.
chipjarred/SwizzleHelper

SwizzleHelper

SwizzleHelper is a Swift Package that helps make swizzling (aka "monkey patching") Objective-C methods easier in Swift, and now it includes support for attaching key-value pairs to an Objective-C class or instance of a class.

Swizzling

Swizzling is a practice that is sometimes used in Objective-C to change out the implementations of methods with another to hook functionality into classes that didn't provide for them. Normally the replacement implementation calls the previous one, either before or after doing it's own thing, in order to preserve the existing functionality. It's generally not done in Swift, and is kind of an extreme method even in Objective-C, but it can be a useful and powerful tool. For example, I used it in combination with an extenson on NSView in my CustomToolTip package to add the ability to attach to any NSView customized tool tips that can contain any NSView as its content without the need to subclass anything or wrap existing objects in a special "tool tippable" object. The effect is you can just assign the tool tip view to a customToolTip property just like you assign a String to the standard toolTip that NSView supports out of the box. I couldn't have made it so simple to use without swizzling.

This only works for @objc methods, which means you can't swizzle Swift-native methods (well, you kind of can, but that involves some especially wicked and tricky manipulation involving the dynamic linker, or the methods to be swizzled have to be marked as dynamic, which means swizzling that method was specifically planned by its author). Another thing to note is that when you call a method directly in Swift, it might not go through the Obj-C messaging mechanism, which means that if you swizzle foo's implementation with another method called bar, when you call foo in Objective-C, it will actually call the implementation that you wrote for bar, because you replaced foo's implementation with it. When you call foo in Swift though, it might very well call the original foo anyway. That's because the compiler often can resolve the exact method that should be called at compile time, but swizzling is a runtime thing. To guarantee same behavior in Swift that you get in Objective-C, you'd have to use NSObject's performSelector methods. Because of this I think it's a good idea to limit swizzling to methods that are called by AppKit or UIKit rather than directly by your code, for example methods of NSResponder or NSView that are called to repond to events or changes in layout. For example, CustomToolTip swizzles updateTrackingAreas, mouseEntered, mouseExited and mouseMoved. Everything else it does is done in pure native Swift methods.

I dont want to encourage anyone to go to swizzling as a first solution, because you are messing with the "natural order" of things when you do this. It's easy to break things in a way that's hard to debug. I may provide more of a "how to" section here at some point in the future, but for now the code is pretty thoroughly documented with doc comments. So if as a last resort, or just out of personal interest, you decide to give it a try, I'll point you to the replaceMethod(_:with:) and callReplacedMethod(for:) methods in the NSObject+Swizzling.swift file. Before you do, spend some time reading the various articles on-line about swizzling, which generally are all about Objective-C, but the principle is exactly the same. Also understand that most wide-spread way to do it also subtly wrong. I recommend putting this blog on your reading list to understand why.

The current state of this package is only a bit more than I needed for CustomToolTip. It only handles forwarding to replaced method implementations that take either no parameters or a single NSObject parameter and return nothing, or which take no parameters and return an NSObject. I'll expand that as I have need; however, if you need more, you'll need to add functions callIMP... functions to swizzleHelper.m and swizzleHelper.h, as well has appropriate callReplacedMethod... methods in NSObject+Swizzling.swift.

If this repo helps you get your swizzling to work, and you find that for your use case you had to add forwarding calls or anything else directly related to swizzling, please consider contributing that part of your code to help the next programmer. While we shouldn't promote swizzling as a "go to" solution, for those who must, we can at least assemble what's needed to do it correctly and reliably in Swift, and in the process make the world a bit less buggy.

Associated Values

Associated values are values you can associate wtih a particular subclass of NSObject or with an instance of any NSObject. They work as key-value pairs, using a String as the key. In cases where you can use a property instead, you probably should. However, Swift extensions cannot have stored properties, which is where associated values come in handy, because you can use a computed property that sets and gets an associated value to behave as though you did have a stored property.

You do this through the associatedValues instance property:

extension NSView
{
    // This will effectively add a stored property to all NSViews
    var shouldBlurContents: Bool
    {
        get { associatedValues["shouldBlurContents"] as? Bool ?? false }
        set { associatedValues["shouldBlurContents"] = newValue }
    }
}

You can also use associated values on the class; however, they do not behave polymorphically.

class BaseClass: NSObject { }
class Subclass: BaseClass { }

BaseClass.associatedValues["animal"] = "dog"
BaseClass.associatedValues["plant"] = "hibiscus"
Subclass.associatedValues["animal"] = "cat"
Subclass.associatedValues["protozoa"] = "amoeba"

// prints "dog"
print("\(BaseClass.associatedValues["animal"] as? String ?? "nil")")

// prints "hibiscus"
print("\(BaseClass.associatedValues["plant"] as? String ?? "nil")")

// prints "nil"
print("\(BaseClass.associatedValues["protozoa"] as? String  ?? "nil")")

// prints "cat"
print("\(Subclass.associatedValues["animal"] as? String  ?? "nil")")

// prints "nil"
print("\(Subclass.associatedValues["plant"] as? String  ?? "nil")")

// prints "amoeba"
print("\(Subclass.associatedValues["protozoa"] as? String  ?? "nil")")

// class associated values must be retrieved by the class, not an instance of it.
let object = Subclass()

// All of these print "nil"
print("\(object.associatedValues["animal"] as? String  ?? "nil")")
print("\(object.associatedValues["plant"] as? String  ?? "nil")")
print("\(object.associatedValues["protozoa"] as? String  ?? "nil")")

If you want polymorphic behavior for static/class properties, you can implement it yourself by using super.associatedValues in subclasses.

Description

  • Swift Tools 5.3.0
View More Packages from this Author

Dependencies

  • None
Last updated: Mon Mar 18 2024 10:26:15 GMT-0900 (Hawaii-Aleutian Daylight Time)