Do that instead of this
iOS Swift Objective-C Estimated reading time: 9 minutesSometimes we need to replace some functionality in existing code, but the source is unavailable to us due to some reason. As option developers could use method swizzling - overriding or in other words replacing the original method implementation with a custom one.
Such technic a bit dangerous and in most cases non needed at all and can be replaced with one of some more safe alternatives. We could use this possibility if no other way is visible or just to experiment and investigate some functionality.
Well, swizzling for iOS originally available thankfully for Objective-C
Runtime
. Now, when most iOS developers use swift, this language provides a convenient way to use Objective-C
swizzling in it and also introduce it’s own native wat to swizzle methods.
Objective-C
Thus method dispatch is handled in runtime by Objective-C
, this allows us to replace any method implementation in runtime before it will be called.
Dispatch system use class object’s isa
pointer to get metaclass object. The next step is checking the method’s table for the selected metaclass object (or its superclass if not found).
The trick with swizzling is that when we use it, we exchange the identifiers of two methods so they point to each other’s implementations. As result at runtime, we can call replaced implementation instead of the original one.
To understand how it works we can check an opensource file for runtime.h
where:
method_name
- an opaque type that represents a method selector (more)
SEL
is a string that hashed according to the method name
method_types
- encoded the return and argument types for method in a character string (more)method_imp
- a pointer to the start of a method implementation (typedef id (*IMP)(id self,SEL _cmd,…);
) (more)
few methods can have same
SEL
, to resolve this collisionIMP
used - as u can see in params ofIMP
there is a place forSEL
.
All this combination used to introduce the Method
(the type that describes the methods in the class):
To get Method in Objective-C runtime we can use something like this
Now u can see, that to change implementation we only need to replace Method
’s method_imp
is of type IMP
using method_setImplementation
.
Example
The simplest example would be to swizzle some method from UIViewController
, for example, viewDidLoad
.
When u run this code, u should see console output similar to this:
2021-01-09 22:36:46.733595+0200 swizzlingObjC[43427:4884488] my viewDidLoad called
Pitfall
The downside of such swizzling is that if we would like to call the original method then we should execute the swizzled method instead, but _cmd
that will be passed to the original method will be from a swizzled method. This may be a problem (some part of functionality may depend on _cmd
).
If we would like to avoid this behavior, we could use the C function for swizzling as suggested by Bryce Buchanan in his article available here.
He suggests to use C function that simulates a method (An Objective-C method is simply a C function that takes at least two arguments—self and _cmd. Apple) and also leaves no trace (such as additional selector).
Be careful: This is a sample code that does not contain protection from double swizzling. If u execute this code twice - then u will swizzle IMP by the same IMP and it will cause an infinite loop…
Swift
Swift
- is a powerful yet young programming language. It contains a bunch of additions that make programming comfortable and functional. Most swift developers come to it from Objective-C, so we know the power of Runtime
. Thus we change the language, the problems we solve are still the same, so the same situations that may require swizzling may occur. The only question - how.
Objective-C Runtime usage
The simplest approach - is to use Runtime
from Objective-C
in Swift
:
And then somewhere before creating ViewController
:
Unfortuantly
+load
is not available in Swift:Method ‘load()’ defines Objective-C class method ‘load’, which is not permitted by Swift
Thus this is works, we still need to use Objective-C
Runtime
and as u saw @obj
and dynamic
attributes for the method. Another moment - we can’t use it if the object is not inherited from NSObject
.
While the
@objc
attribute exposes yourSwift
API to theObjective-C
runtime, it does not guarantee dynamic dispatch of a property, method, subscript, or initializer. TheSwift
compiler may still devirtualize or inline member access to optimize the performance of your code, bypassing theObjective-C
runtime. When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched. Because declarations marked with the dynamic modifier are dispatched using theObjective-C
runtime, they’re implicitly marked with the@objc
attribute.Requiring dynamic dispatch is rarely necessary. However, you must use the dynamic modifier when you know that the implementation of an API is replaced at runtime. For example, you can use the
method_exchangeImplementations
function in theObjective-C
runtime to swap out the implementation of a method while an app is running. If theSwift
compiler inlined the implementation of the method or devirtualized access to it, the new implementation would not be used.
Swift native swizzling
From Swift 5.1
native functions and method swizzling was introduced.
Pitch available here
Now we have @_dynamicReplacement(for:)
which can handle magic for us.
Note: u can’t declare
@_dynamicReplacement(for: original)
in class - there is not much sense in it and as result u receive an errorDynamicReplacement(for:) of ‘replacement’ is not defined in an extension or at the file level
Here also dynamic
attribute used as before.
u can skip this attribute if u use
-enable-dynamic-replacement-chaining
compilation flag
Now u may think - what if someone defined few functions as a replacement for one function?
Be default u receive output:
- original
- replacement2 // the latest defined
But there is 2 behavior of swizzling in Swift
:
- default behavior
- chaining behavior
The difference is simple - if u have multiply swizzling for the same function/method, with chaining behavior they will be called one-by-one and with default behavior - only first-one.
To enable chaining go to BuildSetting->Other Swift Flags -> add -Xfrontend -enable-dynamic-replacement-chaining
.
Build and run, the output will be as following:
- original
- replacement
- replacement2
Now u have few options to select.
Here is an excellent article about both behaviors and how it works under the hood.
Notes
- Swizzling is danger operation - always try to avoid it unless u understand the process and there is no other way
- With swizzling - make sure that u call it only once - in other cases, u will get an unexpected result
- in Objective-C use
+load
method for u’r class - remember about an exchange of
IMP
- If you want to swizzle, the best outcome is to leave no trace source
- Follow DRY and extract your swizzling code into a category or extension
- Swizzling is not atomic
- Difficult to debug
- Possible naming conflicts
- If u have a few swizzled methods - the order is matter
Resources
- Objective-C runtime
- SEL, Method, IMP
- Monkey-Patching iOS with Objective-C Categories Part III: Swizzling
- The Right Way to Swizzle in Objective-C
- Method swizzling
- SO: Danger with swizzling
- MethodSwizzling
- Using Swift with Cocoa and Objective-C
- SF: How to use runtime in Swift?
- SF: Dynamic method replacement
Share on: