Tired Of Objc Method Swizzling? Check Out Swifty Alternative.

Kiarash Vosough
Divar Mobile Engineering
4 min readDec 17, 2022

In this article, I will demonstrate an alternative to Objc method swizzling, with extension even for swift’s structs and enums.

Recall Old-School Objc Method Swizzling

If you are an iOS Developer, you have inevitably used Method Swizzling so-far. Although it is not a safe API, a large group of libraries have used it for some reasons. Let's see the old-school way of method swizzling.

class SomeThing: NSObject {
@objc dynamic
func original(x: Int) {
print("original called")
}
}

extension SomeThing {
@objc dynamic
func replacement(x: Int) {
print("replacement called")
}
}

func beforeAfter() {
let thing = SomeThing()
thing.original(x: 10) // calls original(x:)

// extract selector of the original method
let origMethod = #selector(SomeThing.original(x:))

// extract selector of the replacement method
let replacementMethod = #selector(SomeThing.replacement(x:))

// get the method from the selector and object
let method: Method? = class_getInstanceMethod(SomeThing.self, origMethod)

// get the replacement method from the selector and object
let swizzleMethod: Method? = class_getInstanceMethod(SomeThing.self, replacementMethod)

// get the implementation of the replacement method
let swizzleImpl = method_getImplementation(swizzleMethod!)

// change implementation
method_setImplementation(method!, swizzleImpl);

thing.original(x: 10) // calls replacement(x:)
}

Writing such a long code to change implementation with Objc runtime API is not so convenient.

About the Downsides Of This Method:

  • It’s very hard to use, there is zero type safety.
  • If you forget to mark the entity as dynamic the swizzling won't take effect, because Swift code won’t necessarily call through the Objective-C entry point.
  • It only works for @objc entities, which limits it to methods of classes that can be expressed in the subset that’s exposed to Objective-C.

Swifty Method To Swizzling

This Method extends a similar functionality to all Swift classes, structs, and enums. To allow the runtime to replace a method’s, property’s, initializer’s, or subscript’s implementation mark it with dynamic.

In my experience, do not use the playground to test these codes as it throws errors and the code won’t be compiled. Create a macOS-Terminal project for simplicity.

struct SomeThing {
dynamic var someNumber: Int {
return 10
}

let c: Int

dynamic init(num: Int) {
c = num
}
}

To replace the implementation write a method of the same type inside the same scope and mark the method with the @dynamicReplacement(for:) attribute specifying the original method.

extension SomeThing {
@_dynamicReplacement(for: init(num:))
init(number: Int) {
self.c = number + 1
}

@_dynamicReplacement(for: someNumber())
var newNumber: Int {
return 42
}
}

The Swift runtime will perform the replacement on the application start/loading of the shared library containing the replacement. That the replacement happens at the program start (or loading a shared library), instead of at an arbitrary point in time.

Tips To Use

First:

The @dynamicReplacement(for:) attributes indicate which dynamic declaration is replaced by the new declaration. This declaration must have the same type and be on an extension of the owning type or at module scope for dynamic non-member declarations.

// Module A
extension Set where Element: SomeProtocol {
dynamic func foo() { }
dynamic func bar() { }
}

// Module B
extension Set {
@_dynamicReplacement(for: foo())
dynamic func myFoo() { } // ERROR: signature of MyFoo doesn't match the signature of foo
}

// Module B
extension Set where Element: SomeProtocol {
@_dynamicReplacement(for: bar())
dynamic func myBar() { } // okay: signatures match
}

You might wonder why Module B has an error, that’s because the constraint that foo() and bar() were declared for, is not met. This ensures that the code has still some boundaries and we have restrictions for swizzling. As Comparison, This couldn’t be achieved with old Objc APIs like the below:

let original = #selector(Set<Element>.add()) where Element: SomeProtocol
let replacement = #selector(Set<Element>.add()) where Element: SomeProtocol
let success = swift_method_exchange(original, replacement)

Second:

A call to the original function from within its replacement will call the original function definition, not recurse to itself. For example:

// Module A
dynamic func call() -> Int {
return 20
}

// Module B
@dynamicReplacement(for: theAnswer())
dynamic func mycall() {
return call() + 22
}

UseCases

  1. Add a new implementation and retain the original method to call inside the replaced method.
  2. Can be used for testing and mocking.
  3. Introspecting frameworks and changing their behaviors.

As Swift evolves, we should evolve too, moving from old unsafe APIs to the Swifty Alternatives.🔥

Hope this was helpful and extends your swift knowledge. Have a great weekend🥳🥳🥳🥳🥳🥳🥳

Check My Linkedin for new updates and swift stories.🫡

--

--

Kiarash Vosough
Divar Mobile Engineering

iOS Developer and Swift Lover with over 5 years of experience on Apple platforms. Find me on Any Social Media by searching Kiarash Vosough:)