Method swizzling in Swift and ObjectiveC

Method Swizzling is the ability that Objective-C Runtime gives us, to switch the implementation of an existing selector at runtime.

I came across an article in NSHipster which does method swizzling. The link to the article is here https://nshipster.com/swift-objc-runtime/ For convenience or to work around a bug in a framework, or because there’s just no other way, you need to modify the behavior of an existing class’s methods. Method swizzling lets you swap the implementations of two methods, essentially overriding an existing method with your own while keeping the original around.

The article uses dispatch_once to perform method swizzling. This call is deprecated in Swift 5.x

DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
DISPATCH_SWIFT3_UNAVAILABLE("Use lazily initialized globals instead")
void
dispatch_once(dispatch_once_t *predicate,
		DISPATCH_NOESCAPE dispatch_block_t block);

I replaced this call with a static variable to accomplish the same. I am replacing the method viewWillAppear. Swizzling happens in loadView()

extension UIViewController {
	@objc func rf_viewWillAppear(_ animated: Bool) {
		self.rf_viewWillAppear(animated)
		print("rf_viewWillAppear: \(self)")
    }
}
class SwipeController : UIViewController {
	static var initOnce : SwipeController?
	func setupMethodSwizzle() {
		let originalSelector = #selector(viewWillAppear(_:))
		let swizzledSelector = #selector(rf_viewWillAppear(_:))

		let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector)
		let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)

		let didAddMethod = class_addMethod(UIViewController.self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))

		if didAddMethod {
			class_replaceMethod(UIViewController.self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
		} else {
			method_exchangeImplementations(originalMethod!, swizzledMethod!);
		}
	}
	
	override func loadView() {
		super.loadView()
		if(SwipeController.initOnce == nil) {
			SwipeController.initOnce = self
			self.setupMethodSwizzle()
		}
	}

Point to note here. If a method is marked objc_direct, you won’t be able to swizzle the method. I will be updating this post with a link to explain this. More details about this api is given here https://reviews.llvm.org/rG11d47b3d553c6c3888745e96f460855aae541c48

Now, lets try to the same in objective-C.

//
//  UIView+HandleLoads.m
//  testobjc
//
//  Created by darshan on 6/16/21.
//

#import "UIView+HandleLoads.h"
#include <objc/runtime.h>
@implementation UIView (HandleLoads)
+(void)load {
	static dispatch_once_t once_token;
		dispatch_once(&once_token,  ^{
			SEL viewWillAppearSelector = @selector(addSubview:);
			SEL viewWillAppearLoggerSelector = @selector(logged_addSubview:);
			Method originalMethod = class_getInstanceMethod(self, viewWillAppearSelector);
			Method extendedMethod = class_getInstanceMethod(self, viewWillAppearLoggerSelector);
			method_exchangeImplementations(originalMethod, extendedMethod);
		});
}

-(void) logged_addSubview:(UIView*)view {
	[self logged_addSubview:view];
	NSLog(@"logged view did appear for %@", [self class]);
}
@end

It may appear that the above code will result in an infinite loop. Surprisingly, it won’t. In the process of swizzling, logged_addSubview: has been reassigned to the original implementation of UIView -addSubview:. It’s good programmer instinct for calling a method on self in its own implementation to raise a red flag, but in this case, it makes sense if we remember what’s really going on. However, if we were to call addSubview: in this method, it would cause an infinite loop, since the implementation of this method will be swizzled to the addSubview: selector at runtime. Next call the above code from UIViewController

- (void)viewDidLoad {
	[super viewDidLoad];
	// Do any additional setup after loading the view.
	UIView* view = [[UIView alloc] init];
	view.frame = self.view.frame;
	view.backgroundColor = UIColor.blueColor;
	[self.view addSubview:view];
}

One response

  1. […] Before you start reading this tutorial, please read about method swizzling. https://redflowerinc.com/method-swizzling-in-swift/ […]

Leave a Reply

Your email address will not be published. Required fields are marked *