46

How can I implement method swizzling in Swift 3.0 ?

I've read nshipster article about it, but in this code's chunk

struct Static {
    static var token: dispatch_once_t = 0
}

the compiler gives me an error

dispatch_once_t is unavailable in Swift: Use lazily initialized globals instead

Tikhonov Aleksandr
  • 13,945
  • 6
  • 39
  • 53
  • The error message is a bit obtuse; it's fine to use a `static` variable, as `static` variables are automatically created exactly once, which occurs when they are first accessed (i.e., lazily). `static var token = 0` is the correct way to do this in Swift 3. [Source](https://swift.org/migration-guide-swift3/) – BallpointBen Jul 14 '17 at 19:27

6 Answers6

64

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)")
    }
}
Community
  • 1
  • 1
Tikhonov Aleksandr
  • 13,945
  • 6
  • 39
  • 53
  • 1
    In this context, is there ever a time when `class_addMethod()` returns `true`? Because the swift `#selector` requires the associated method to exist, `originalSelector` will always exist, causing `class_addMethod` to "fail". I found it safe to include only a call to `method_exchangeImplementations()`, and omit the calls to `classAddMethod()` and `class_replaceMethod()`. – Kevin Owens Nov 26 '16 at 17:50
  • Should the last snippet work in Playground? I moved `swizzling` under the extension so the compiler stops complaining but it never executes it after calling `Programmer().foo(true)` or `Person().foo(true)`. – DevAndArtist Jan 26 '17 at 12:13
  • One suggestion: you can write `"class: \(type(of: self))"` to get a consistent output without any prefix like `__lldb_expr_1061.` – DevAndArtist Jan 26 '17 at 12:21
  • 13
    The method ˆinitialize()` will be disallowed in future versions of Swift. **Swift 3.1 Changelog:** Swift will now warn when an NSObject subclass attempts to override the class initialize method. Swift doesn't guarantee that references to class names trigger Objective-C class realization if they have no other side effects, leading to bugs when Swift code attempted to override initialize. – Alessandro Feb 28 '17 at 15:12
  • 3
    @Alessandro any suggestions on how to perform swizzling given these restrictions? – Alexander Telegin May 10 '17 at 18:40
  • 2
    Bump. Any ideas on next-best-place to perform the swizzle? – MikeyWard May 26 '17 at 14:16
  • @Alessandro any idea what to do with this? – ilan Jun 21 '17 at 07:02
  • 2
    You could add add a method to the app delegate to be executed when application(_:didFinishLaunchingWithOptions:) is called. Check the AppDelegate in the [Kickstarter App repo](https://github.com/kickstarter/ios-oss/blob/1f5643f6a769995ccd1bb3826699745e64597ab7/Kickstarter-iOS/AppDelegate.swift); in particular the UIViewController.doBadSwizzleStuff() implementation. @ilan – Alessandro Jun 21 '17 at 07:17
  • This is not a safe way to swizzle for the reasons elaborated here: https://blog.newrelic.com/2014/04/16/right-way-to-swizzle/ – Werner Altewischer Jan 11 '18 at 13:05
  • so based on what flag does it switch between original and swizzled method? – mfaani Apr 14 '18 at 19:31
  • why is it marked as "answer"? Currently you can't even write a method named `initialize()` – Vyachaslav Gerchicov Jul 11 '18 at 14:00
40

@TikhonovAlexander: Great answer

I modified the swizzler to take both selectors and made it more generic.

Swift 4 / Swift 5

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

extension UIView {
    
    static let classInit: Void = {            
        let originalSelector = #selector(layoutSubviews)
        let swizzledSelector = #selector(swizzled_layoutSubviews)
        swizzling(UIView.self, originalSelector, swizzledSelector)
    }()
    
    @objc func swizzled_layoutSubviews() {
        swizzled_layoutSubviews()
        print("swizzled_layoutSubviews")
    }
    
}

// perform swizzling in AppDelegate.init()

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    override init() {
        super.init()
        UIView.classInit
    }

}

Swift 3

private let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in
    let originalMethod = class_getInstanceMethod(forClass, originalSelector)
    let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
    method_exchangeImplementations(originalMethod, swizzledMethod)
}

// perform swizzling in initialize()

extension UIView {
    
    open override class func initialize() {
        // make sure this isn't a subclass
        guard self === UIView.self else { return }
        
        let originalSelector = #selector(layoutSubviews)
        let swizzledSelector = #selector(swizzled_layoutSubviews)
        swizzling(self, originalSelector, swizzledSelector)
    }
    
    func swizzled_layoutSubviews() {
        swizzled_layoutSubviews()
        print("swizzled_layoutSubviews")
    }
    
}
efremidze
  • 2,640
  • 1
  • 25
  • 35
  • I tried to swizzle UIColor description selector using swift 4 code mentioned in this post. `let originalSelector = #selector(description) let swizzledSelector = #selector(colorDescription) swizzling(UIColor.self, originalSelector, swizzledSelector)` But it was not working. I had to use `let instance: UIColor = UIColor.red let aClass: AnyClass! = object_getClass(instance)` instead of `UIColor.self`. Why is it so? – abhimuralidharan Dec 20 '17 at 12:01
  • 1
    This is not a safe way to swizzle methods for the reasons elaborated on here: https://blog.newrelic.com/2014/04/16/right-way-to-swizzle/ – Werner Altewischer Jan 11 '18 at 13:04
  • Why are you using blocks and not methods? Because you just copied something together you don't understand. The blocks where used because reason explained in the blog post... How you implemented it you just walk around that and make it useless. Don't adopt something because someone did it without understand it why they did it. – hashier Jan 23 '18 at 13:28
  • 1
    @hashier, why do you argue with blocks or methods? From my understanding, using the classInit block is fine, it can guarantee that classInit can be executed only once. Could you please show me the post link you mentioned? – DàChún Feb 01 '18 at 11:36
  • @User9527 right, the block is so it only gets executed once but then it is used as a method here. So it makes sense for the parts that you only want to get executed once but the method you call for every swizzle could be a normal method so you could swizzle another method again. – hashier Feb 12 '18 at 14:40
  • so based on what flag does it switch between original and swizzled method? – mfaani Apr 14 '18 at 19:31
  • 1
    The Swift 4 doesn't compile. You need to add @objc to the `swizzled_layoutSubviews` and do optional checks on the methods inside of `swizzling`. – Jonny Jul 30 '18 at 02:14
11

Swizzling in Playground
Swift 5

import Foundation
class TestSwizzling : NSObject {
    @objc dynamic func methodOne()->Int{
        return 1
    }
}

extension TestSwizzling {
    @objc func methodTwo()->Int{
        // It will not be a recursive call anymore after the swizzling
        return 2
    }
    //In Objective-C you'd perform the swizzling in load(),
    //but this method is not permitted in Swift
    func swizzle(){
        let i: () -> () = {
            let originalSelector = #selector(TestSwizzling.methodOne)
            let swizzledSelector = #selector(TestSwizzling.methodTwo)
            let originalMethod = class_getInstanceMethod(TestSwizzling.self, originalSelector);
            let swizzledMethod = class_getInstanceMethod(TestSwizzling.self, swizzledSelector)
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
            print("swizzled!")
        }
        i()
    }
}

var c = TestSwizzling()
print([c.methodOne(), c.methodTwo()])  // [1, 2]
c.swizzle()                            // swizzled!
print([c.methodOne(), c.methodTwo()])  // [2, 1]
Dharay
  • 195
  • 1
  • 7
  • Result print([c.methodOne(), c.methodTwo()]) ==> // [1, 2]. Because you declared methodTwo() return 2. Please update your code to avoid misleading. – Giang Jan 18 '21 at 08:03
1

Swift swizzling

@objcMembers
class sA {
    dynamic
    class func sClassFooA() -> String {
        return "class fooA"
    }
    
    dynamic
    func sFooA() -> String {
        return "fooA"
    }
}

@objcMembers
class sB {
    dynamic
    class func sClassFooB() -> String {
        return "class fooB"
    }
    
    dynamic
    func sFooB() -> String {
        return "fooB"
    }
}

Swizzling.swift

import Foundation

@objcMembers
public class Swizzling: NSObject {
    
    public class func sExchangeClass(cls1: AnyClass, sel1: Selector, cls2: AnyClass, sel2: Selector) {
        
        let originalMethod = class_getClassMethod(cls1, sel1)
        let swizzledMethod = class_getClassMethod(cls2, sel2)
        
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }

    public class func sExchangeInstance(cls1: AnyClass, sel1: Selector, cls2: AnyClass, sel2: Selector) {
        
        let originalMethod = class_getInstanceMethod(cls1, sel1)
        let swizzledMethod = class_getInstanceMethod(cls2, sel2)
        
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
}

Using via Swift

func testSExchangeClass() {
    Swizzling.sExchangeClass(cls1: sA.self, sel1: #selector(sA.sClassFooA), cls2: sB.self, sel2: #selector(sB.sClassFooB))
    
    XCTAssertEqual("class fooB", sA.sClassFooA())
}

func testSExchangeInstance() {
    Swizzling.sExchangeInstance(cls1: sA.self, sel1: #selector(sA.sFooA), cls2: sB.self, sel2: #selector(sB.sFooB))
    
    XCTAssertEqual("fooB", sA().sFooA())
}

[Add Objective-C as an consumer]

using via Objective-C

- (void)testSExchangeClass {
    [Swizzling sExchangeClassWithCls1:[cA class] sel1:@selector(cClassFooA) cls2:[cB class] sel2:@selector(cClassFooB)];
    
    XCTAssertEqual(@"class fooB", [cA cClassFooA]);
}

- (void)testSExchangeInstance {
    [Swizzling sExchangeInstanceWithCls1:[cA class] sel1:@selector(cFooA) cls2:[cB class] sel2:@selector(cFooB)];
    
    XCTAssertEqual(@"fooB", [[[cA alloc] init] cFooA]);
}

[Objective-C swizzling]

yoAlex5
  • 29,217
  • 8
  • 193
  • 205
0

Swift 5.1

Swift use Objective-C runtime feature to make method swizzling. Here you are two ways.

Note: open override class func initialize() {} is not allowed anymore.

  1. class inherit NSObject, and method must have dynamic attribute

    class Father: NSObject {
       @objc dynamic func makeMoney() {
           print("make money")
       }
    }
    extension Father {
       static func swizzle() {
           let originSelector = #selector(Father.makeMoney)
           let swizzleSelector = #selector(Father.swizzle_makeMoney)
           let originMethod = class_getInstanceMethod(Father.self, originSelector)
           let swizzleMethod = class_getInstanceMethod(Father.self, swizzleSelector)
           method_exchangeImplementations(originMethod!, swizzleMethod!)
       }
       @objc func swizzle_makeMoney() {
           print("have a rest and make money")
       }
    }
    Father.swizzle()
    var tmp = Father()
    tmp.makeMoney() //  have a rest and make money
    tmp.swizzle_makeMoney() // make money
    
    1. Use @_dynamicReplacement(for: )

       class Father {
           dynamic func makeMoney() {
               print("make money")
           }
       }
       extension Father {
           @_dynamicReplacement(for: makeMoney())
           func swizzle_makeMoney() {
               print("have a rest and make money")
           }
       }
       Father().makeMoney() // have a rest and make money
    
mistdon
  • 1,773
  • 16
  • 14
  • If I use @_dynamicReplacement (for:) in a pod, when I run it in the simulator, I will get an error: "unsupported relocation with subtraction expression, symbol '' can not be undefined in a subtraction expression". Is there a way to solve or avoid it? – Rakuyo Apr 02 '20 at 07:04
  • I asked this [question](https://stackoverflow.com/questions/60986318/dynamicreplacementfor-error-in-simulator) – Rakuyo Apr 02 '20 at 07:30
0

Try this framework: https://github.com/623637646/SwiftHook

let object = MyObject()
let token = try? hookBefore(object: object, selector: #selector(MyObject.noArgsNoReturnFunc)) {
    // run your code
    print("hooked!")
}
object.noArgsNoReturnFunc()
token?.cancelHook() // cancel the hook

It's very easy to hook methods in Swift.

Yanni
  • 580
  • 2
  • 6
  • 21