How to Implement Method Swizzling Swift 3.0

How to implement method swizzling swift 3.0?

First of all dispatch_once_t is unavailable in Swift 3.0.
You can choose from two alternatives:

  1. Global variable

  2. Static property of struct, enum or class

For more details, see that Whither dispatch_once in Swift 3

For different purposes you must use different implementation of swizzling

  • Swizzling CocoaTouch class, for example UIViewController;
  • Swizzling custom Swift class;

Swizzling CocoaTouch class

example swizzling viewWillAppear(_:) of UIViewController using global variable

private let swizzling: (UIViewController.Type) -> () = { viewController in

let originalSelector = #selector(viewController.viewWillAppear(_:))
let swizzledSelector = #selector(viewController.proj_viewWillAppear(animated:))

let originalMethod = class_getInstanceMethod(viewController, originalSelector)
let swizzledMethod = class_getInstanceMethod(viewController, swizzledSelector)

method_exchangeImplementations(originalMethod, swizzledMethod) }

extension UIViewController {

open override class func initialize() {
// make sure this isn't a subclass
guard self === UIViewController.self else { return }
swizzling(self)
}

// MARK: - Method Swizzling

func proj_viewWillAppear(animated: Bool) {
self.proj_viewWillAppear(animated: animated)

let viewControllerName = NSStringFromClass(type(of: self))
print("viewWillAppear: \(viewControllerName)")
}
}

Swizzling custom Swift class

To use method swizzling with your Swift classes there are two requirements that you must comply with (for more details):

  • The class containing the methods to be swizzled must extend NSObject
  • The methods you want to swizzle must have the dynamic attribute

And example swizzling method of custom Swift base class Person

class Person: NSObject {
var name = "Person"
dynamic func foo(_ bar: Bool) {
print("Person.foo")
}
}

class Programmer: Person {
override func foo(_ bar: Bool) {
super.foo(bar)
print("Programmer.foo")
}
}

private let swizzling: (Person.Type) -> () = { person in

let originalSelector = #selector(person.foo(_:))
let swizzledSelector = #selector(person.proj_foo(_:))

let originalMethod = class_getInstanceMethod(person, originalSelector)
let swizzledMethod = class_getInstanceMethod(person, swizzledSelector)

method_exchangeImplementations(originalMethod, swizzledMethod)
}

extension Person {

open override class func initialize() {
// make sure this isn't a subclass
guard self === Person.self else { return }
swizzling(self)
}

// MARK: - Method Swizzling

func proj_foo(_ bar: Bool) {
self.proj_foo(bar)

let className = NSStringFromClass(type(of: self))
print("class: \(className)")
}
}

UIView method swizzling swift 3

The problem is that awakeFromNib is a method of NSObject and
not of UIView. Your code swizzles the NSObject method with a
method of UIView, and calling the original method crashes
when the swizzled method is called on UINavigationController
(or any other subclass of NSObject which is not a subclass of UIView).

The solution is to try to add the swizzled method with the
original name first (as described in http://nshipster.com/method-swizzling/):

private let swizzling: (UIView.Type) -> () = { view in
let originalSelector = #selector(view.awakeFromNib)
let swizzledSelector = #selector(view.swizzled_localization_awakeFromNib)

let originalMethod = class_getInstanceMethod(view, originalSelector)
let swizzledMethod = class_getInstanceMethod(view, swizzledSelector)

let didAddMethod = class_addMethod(view, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(view, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}

Swift - method swizzling

Extend your class:

extension YourClassName {
static let classInit: () -> () = {
let originalSelector = #selector(originalFunction)
let swizzledSelector = #selector(swizzledFunction)
swizzle(YourClassName.self, originalSelector, swizzledSelector)
}

@objc func swizzledFunction() {
//Your new implementation
}
}

Your class (YourClassName) should inherit from NSObject and originalSelector should be a dynamic method.

swizzle is a closure that exchanges the implementations:

private let swizzle: (AnyClass, Selector, Selector) -> () = { fromClass, originalSelector, swizzledSelector in
guard
let originalMethod = class_getInstanceMethod(fromClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(fromClass, swizzledSelector)
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}

You could define swizzle in the class where you are going to do the swizzling. For example the AppDelegate class definition. And then do the swizzling in AppDelegate.init():

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
override init() {
super.init()
YourClassName.classInit()
}
}

Example

Here is a concrete example of swizzling, it exchanges the implementation of two methods that take one argument:

class Example: NSObject {
@objc dynamic func sayHi(to name: String) {
print("Hi", name)
}
}

extension Example {
public class func swizzleMethod() {
guard
let originalMethod = class_getInstanceMethod(Example.self, #selector(Example.sayHi(to:))),
let swizzledMethod = class_getInstanceMethod(Example.self, #selector(Example.sayHello(to:)))
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}
@objc func sayHello(to name: String) {
print("Hello", name)
}
}

Example.swizzleMethod()

let a = Example()
a.sayHi(to: "Nitish") //Hello Nitish

Method swizzling in Swift

Objective-C, which uses dynamic dispatch supports the following:

Class method swizzling:

The kind you're using above. All instances of a class will have their method replaced with the new implementation. The new implementation can optionally wrap the old.

Isa-pointer swizzling:

An instance of a class is set to a new run-time generated sub-class. This instance's methods will be replaced with a new method, which can optionally wrap the existing method.

Message forwarding:

A class acts as a proxy to another class, by performing some work, before forwarding the message to another handler.

These are all variations on the powerful intercept pattern, which many of Cocoa's best features rely on.

Enter Swift:

Swift continues the tradition of ARC, in that the compiler will do powerful optimizations on your behalf. It will attempt to inline your methods or use static dispatch or vtable dispatch. While faster, these all prevent method interception (in the absence of a virtual machine). However you can indicate to Swift that you'd like dynamic binding (and therefore method interception) by complying with the following:

  • By extending NSObject or using the @objc directive.
  • By adding the dynamic attribute to a function, eg public dynamic func foobar() -> AnyObject

In the example you provide above, these requirements are being met. Your class is derived transitively from NSObject via UIView and UIResponder, however there is something odd about that category:

  • The category is overriding the load method, which will normally be called once for a class. Doing this from a category probably isn't wise, and I'm guessing that while it might have worked before, in the case of Swift it doesn't.

Try instead to move the Swizzling code into your AppDelegate's did finish launching:

//Put this instead in AppDelegate
method_exchangeImplementations(
class_getInstanceMethod(UINavigationBar.self, "sizeThatFits:"),
class_getInstanceMethod(UINavigationBar.self, "sizeThatFits_FixedHeightWhenStatusBarHidden:"))

Method Swizzling with Swift 5.5

That's because

@objc internal func log_initWithString(string URLString: String)

is exposed to Objective-C as log_initWithStringWithString: and not as log_initWithString:.

Obvious fix is:

...
let swizzled = Selector("log_initWithStringWithString:")
...

To have better compile time checks on that you can use this syntax:

let original = #selector(NSURL.init(string:))
let swizzled = #selector(NSURL.log_initWithString(string:))

This will compile, but there is at least one thing left to fix - swizzled method return value. In your example:

@objc internal func log_initWithString(string URLString: String) {
NSLog("Hello from initWithString")

return log_initWithString(string: URLString)
}

returns nothing, while NSURL's init is supposed to return NSURL, so the fix is:

@objc internal func log_initWithString(string URLString: String) -> NSURL {
...


Related Topics



Leave a reply



Submit