Ever wonder how to change memory pointers during runtime in your iOS app? Probably not, but just incase this is how you do it!
What is swizzling?
Let’s say we have method A and method B. If we swizzled these two methods it would cause their reference pointers to swap. Or in simpler terms calling A would actually trigger B’s implementation and vice versa. This is a fairly simple concept but the implementation can be a little confusing if one is not familiar with it and has never seen it implemented before.
Here is an example of swizzling Bundle’s method object(forInfoDictionaryKey:)
:
extension Bundle {
@objc private func hijacked_call_to_object(forInfoDictionaryKey key: String) -> Any? {
// Do whatever you want before calling object(forInfoDictionaryKey:))
let response = hijacked_call_to_object(forInfoDictionaryKey: key)
// Do whatever you want after calling object(forInfoDictionaryKey:))
return response
}
private static let swizzleObjectForInfoDictionaryKeyImplementation: Void = {
let aClass: AnyClass! = object_getClass(Bundle.main)
if let originalMethod = class_getInstanceMethod(aClass, #selector(object(forInfoDictionaryKey:))),
let replacementMethod = class_getInstanceMethod(aClass, #selector(hijacked_call_to_object(forInfoDictionaryKey:))) {
// Here is where the reference pointers are swapped
method_exchangeImplementations(originalMethod, replacementMethod)
}
}
}
What the code above is doing is creating selectors for the magical world of Objective-C and then using the Objective-C runtime methodmethod_exchangeImplementations
it exchanges the implementations of two methods.
Did you notice that in hijacked_call_to_object
it calls itself? This is where often the confusion comes in. This looks like infinite recursions. However what is really happening here is that Bundle’s original implementation is being called. It is not calling itself since the hijacked_call_to_objec
selector is pointing to object(forInfoDictionaryKey:)
. It is always important to leave comments when swizzling explaining this and also the reasons why you are swizzling in the first place. Otherwise this will be a head scratcher for the next person.
This is how you trigger the swizzle:
_ = swizzleObjectForInfoDictionaryKeyImplementation
Should I do this?
Probably not! But, desperate times call for desperate measures. You typically want to avoid doing this unless it is the only option. This concept come from the wild west of Objective-C and hacking the runtime. Swizzling is useful for either fully replacing functionality or adding functionality before or after a method call. The only time I have seen this needed is when a third party SDK required the app to keep config data in info.plist
. There was a scenario where in the future this config could change, possibly rendering older versions of the app useless since info.plist
is read only during runtime. The way we worked around this was getting the config from our back end and hijacking Bundle to return the correct config to the SDK.
What Could Possibly Go Wrong…
If you swizzle a method already swizzled somewhere else it could cause issues. You need to make sure you are not swizzling anything a third party swizzles as well. For example Firebase, a very popular framework, swizzles AppDelegate’s methods.
Swizzling can be hard to understand due to recursion confusion. You can run into naming conflicts which wouldn’t get picked up by the compiler since this is all being done runtime. It can also be hard to debug since your stack trace will have the method names swapped. Of course proper documentation and good method naming can reduce confusion while debugging.
Bonus Round! @_dynamicReplacement
A Swift native way to swizzle was introduced in Swift 5. Here is what it looks like:
dynamic func A() {
print("A")
}
@_dynamicReplacement(for: A)
func B() {
print("B")
A()
}
A() // Prints out "B" then "A"
B() // Prints out "B" then "A"
This syntax is much cleaner! To make a method replaceable you need to add the dynamic
modifier to your method. For the hijacking method you need to use@_dynamicReplacement(for:)
and pass in the method who’s pointer you want to hijack. That’s it!
There are a few major differences to the Objective-C runtime implementation. Here calling A inside B actually points to the implementation of A. However calling A outside of B will always trigger B. Also calling B still points to B’s implementation. Under the hood what is happening is the compiler doesn’t actually swap the implementations. It just keeps track of A’s original implementation and knows in what contexts to use it.
The down side to this implementation is that you cannot use it in the scenario of hijacking Bundle. This is because you need Bundle’s methods to have the dynamic
modifier which they don’t. I am not sure in what scenarios this is useful but I would take the same approach as with the Objective-C runtime solution. Only if all options are out the window and you are desperate.