icon-image
Welcome to Innominds Blog.
Enjoy our insights and engage with us!

Method Swizzling in iOS Development

By Aruna Kumari Yarra,

ImageMethod Swizzling is the ability to change the functionality of an existing selector/method at runtime.

Method Swizzling is also Known as Monkey Patching

Most of the use cases we have found for method swizzling involve extending the functionality of an existing method. Meaning, there is some method that you can’t modify directly, but would like to add additional functionality to (e.g. logging, tracking analytics, injecting JavaScript in WebViews to find JS errors, etc.). It might seem like subclassing would be the most preferred way to do this, and for most scenarios, it probably is, though there are places where swizzling makes more sense to intercept the methods. To get a feel of what those scenarios would look like, let’s look at an example.

Method Swizzling Use case Example

Suppose we want analytics each time the user views a screen, the app should call the API requests for page views tracking.

We could subclass UIViewController with a TrackingViewController and use that as our new base class for view controllers. While this approach is usually reasonable, it may be undesirable for several reasons:

  1. We would need to shim in subclasses for UIViewController, and all of the other controllers that extend it (e.g., UITableViewController, UINavigationViewController, etc.) to ensure that all viewWillAppear calls are tracked.
  2. In order to make use of your tracking functionality, other projects would have to shim in a TrackingViewController as well (e.g., this solution is not easily extensible to other projects). Imagine a situation where you provide an analytics library and the developer will have to subclass your SDK class for every UIViewController.
  3. We have limited ability to hook into the code of 3rd party libraries if necessary. In that case, we can't add our requirement of tracking analytics.

Method swizzling can solve this problem in quite a simple way. All we have to do is to write a custom function _tracked_viewWillAppear then swap it with the original function viewWillAppear 

At run time, calling viewWillAppear on UIViewController calls_tracked_viewWillAppear method where we will have the notification logic.

Now, in the child classes of UIViewControllers, super.ViewWillAppear, Calls the _tracked_viewWillAppear. Now since all the extended/derived controllers like UITableViewController and UINavigationController are subclasses of UIViewController, the same gets called for all the different classes.

To achieve this, we can do the following:

How to Swizzle a Method?

The main function we need to remember is method_exchangeImplementations:

func method_exchangeImplementations(_ originalMethod: Method, _ swizzledMethod: Method)

As the name reflects, the implementations of originalMethod and swizzledMethod get swapped after calling this function. It means that an invocation to originalMethod actually executes the code inside swizzledMethod and vice versa.

  1. Create a category/extension

    It’s a standard practice to create a category method when using method swizzling to extend the existing functionality. For our tracking example, let’s create a UIViewController+Tracking category on UIViewController.

  2. Write the additional functionality

    The next step is to write a category method inside of our UIViewController+Tracking class that includes the functionality we wish to add tracking (in our example). To do this we’ll write a method called _tracked_viewWillAppear.

    Swift

    @objc dynamic func _tracked_viewWillAppear(_ animated: Bool) {
    NSLog("Enter screen: \(type(of: self))")
    _tracked_viewWillAppear(animated)
    }

    Objective-C

    - (void) _tracked_viewWillAppear:(BOOL)animated {
        NSLog(@"Enter screen: %@", [self class]);
        [self _tracked_viewWillAppear:animated];
    }
    

    Note: It may seem this method will run infinitely, but it won’t. Because at runtime _tracked_viewWillAppear: selector would point on viewWillAppear. Then we have an NSLog to log our command. In short, when the view is about to be displayed, the program prints a log, for example, Enter screen: SampleViewController and does what it is supposed to do.

    Now for any viewWillAppear call on UIViewController from our code or from a framework, our swizzled method will be executed.

  3. Swizzle the methods

    The last step is to write the code that actually swaps the memory locations that the two selectors correspond to. This is typically done in the load class method and wrapped in a dispatch_once. In our UIViewController+Tracking class, we’ll add the following.

    Objective-C

    #import <objc/runtime.h>
    #import "UIViewController+Tracking.h"
    
    @implementation UIViewController (Tracking)
    + (void)load {
        static dispatch_once_t once_token;
        dispatch_once(&once_token, ^{
            Class class = [self class];
            SEL defaultSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(_tracked_viewWillAppear:);
            // 1) The IMP of default method will point on UIViewController's
            //viewWillAppear implementation
            Method defaultMethod =
            	class_getInstanceMethod(class, defaultSelector);
            Method swizzledMethod = 
            	class_getInstanceMethod(class, swizzledSelector);
            // 2) Here we add the method defaultSelector with the IMP to point
            //on swizzledMethod's implementation.
            BOOL isMethodExists = 
            	!class_addMethod(class, defaultSelector,
                	method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
            if (isMethodExists) {
                method_exchangeImplementations(defaultMethod, swizzledMethod);
            }
            else {
                // 3) We replace swizzledSelector method with the method that
                //defaultMethod was pointing
                //(The initial value which points on UIViewController's
                //viewWillAppear IMP). 
                //Note that if we run class_getInstanceMethod
                //(class, swizzledSelector);
                //will get the method 
                //that we add point 2 instead of the initial.
                class_replaceMethod(class, swizzledSelector,
                	method_getImplementation(defaultMethod),
                    method_getTypeEncoding(defaultMethod));
            	}
        });
    }
    - (void) _tracked_viewWillAppear:(BOOL)animated {
        NSLog(@"Enter screen: %@", [self class]);
        [self _tracked_viewWillAppear:animated];
    }
    @end
    

    Swift

    static func swizzle() {
            //Make sure This isn't a subclass of UIViewController,
            //So that It applies to all UIViewController childs
            if self != UIViewController.self {
                return
            }
            let _: () = {
                let originalSelector = 
                	#selector(UIViewController.viewWillAppear(_:))
                let swizzledSelector = 
                	#selector(UIViewController._tracked_viewWillAppear(_:))
                let originalMethod = 
                	class_getInstanceMethod(self, originalSelector)
                let swizzledMethod = 
                	class_getInstanceMethod(self, swizzledSelector)
                method_exchangeImplementations(originalMethod!, swizzledMethod!);
            }()
        }
    

    Note: Unlike in Swift3x, in swift 4, we cannot write this swizzling in the initialise method, so we need to write the one static method and we can call it in AppDelegate didFinishLaunchingWithOptions method.

    There are two cases that need to be handled:

    • Need to check whether the method we're swizzling is actually defined in a superclass by using class_addMethod to the target class. If it returns true, then we can use class_replaceMethod to replace with the superclass' implementation so our new version will be able to rightly call the old one.
    • If the method is defined in the target class, then class_addMethod will fail so we need to use method_exchangeImplementations to just swap the new and old versions.

    This code swizzles the method implementation for viewWillAppear with the implementation for _tracked_viewWillAppear. To get this to compile, you need to import objc/runtime.h. Putting it all together, our UIViewController+Tracking class looks like this:

    Objective-C

    #import <objc/runtime.h>
    #import "UIViewController+Tracking.h"
    
    @implementation UIViewController (Tracking)
    + (void)load {
        static dispatch_once_t once_token;
        dispatch_once( &once_token, ^{
            Class class = [self class];
            SEL defaultSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(_tracked_viewWillAppear:);
            // 1) The IMP of default method will point on UIViewController's 
            //viewWillAppear implementation
            Method defaultMethod = 
            	class_getInstanceMethod(class, defaultSelector);
            Method swizzledMethod = 
            	class_getInstanceMethod(class, swizzledSelector);
            // 2) Here we add the method defaultSelector with the IMP to point
            //on swizzledMethod's implementation.
            BOOL isMethodExists = !class_addMethod(class, defaultSelector,
    			method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));
            if (isMethodExists) {
                method_exchangeImplementations(defaultMethod, swizzledMethod);
            }
            else {
                // 3) We replace swizzledSelector method with the method that 
                //defaultMethod was pointing
                //(The initial value which points on UIViewController's
                //viewWillAppear IMP).
                //Note that if we run class_getInstanceMethod
                //(class, swizzledSelector);
                //will get the method that we add point 
                //2 instead of the initial.
                class_replaceMethod(class, swizzledSelector,
    				method_getImplementation(defaultMethod),
                    method_getTypeEncoding(defaultMethod));
            	}
        	});
    }
    - (void) _tracked_viewWillAppear:(BOOL)animated {
        NSLog(@"Enter screen: %@", [self class]);
        [self _tracked_viewWillAppear:animated];
    }
    @end
    

    Swift

    import UIKit
    extension UIViewController {
        @objc dynamic func _tracked_viewWillAppear(_ animated: Bool) {
            NSLog("Enter screen: \(type(of: self))")
            _tracked_viewWillAppear(animated)
        }
    
        static func swizzle() {
            //Make sure This isn't a subclass of UIViewController,
            // So that It applies to all UIViewController childs
            if self != UIViewController.self {
                return
            }
            let _: () = {
                let originalSelector = 
                	#selector(UIViewController.viewWillAppear(_:))
                let swizzledSelector =
                	#selector(UIViewController._tracked_viewWillAppear(_:))
                let originalMethod = 
                	class_getInstanceMethod(self, originalSelector)
                let swizzledMethod = 
                	class_getInstanceMethod(self, swizzledSelector)
                method_exchangeImplementations
                	(originalMethod!, swizzledMethod!);
            }()
        }
    }
    
    

    In AppDelegate we need to call this method in Swift

    
    func application( _ application: UIApplication, 
    			didFinishLaunchingWithOptions 
    			launchOptions: [UIApplication.LaunchOptionsKey: Any]?
                ) -> Bool {
    			   UIViewController.swizzle()
    				// Override point for customization after 
                    // application launch.
    		   	 return true
    }
    

Other Ideas of Using Method swizzling

  • UIButton swizzling to figure out the number of clicks on a button
  • Swizzle over UIView to figure out the hottest area/ most clicked area of the application

Conclusion

Method Swizzling is a dynamic feature that can exchange the implementations of two methods in runtime and it's often used to modify the behavior of framework classes as well. This means it can implement some complex functions conveniently.

It is safe if we use it properly. A simple safety measure you can take is to only swizzle in load. Like many things in programming, it can be dangerous, but understanding the consequences will allow us to use it the right way.

About Innominds

Innominds is a leading Digital Transformation and Product Engineering company headquartered in San Jose, CA. It offers co-creation services to enterprises for building solutions utilizing digital technologies focused on Devices, Apps, and Analytics. Innominds builds better outcomes securely for its clients through reliable advanced technologies like IoT, Blockchain, Big Data, Artificial Intelligence, DevOps and Enterprise Mobility among others. From idea to commercialization, we strive to build convergent solutions that help our clients grow their business and realize their market vision.

Interested! For any demos or project discussions, please write to us at marketing@innominds.com and know more about our offerings.

Topics: Mobility

Aruna Kumari Yarra

Aruna Kumari Yarra

Senior Engineer - Software Engineering

Subscribe to Email Updates

Recent Posts