swifthookaspectswizzlingswizzle

How to hook (swizzle) methods in Swift?


Is there any easy way to hook (swizzle) methods in Swift?


Solution

  • This framework can help: https://github.com/623637646/SwiftHook

    How to use

    For example, this is your class

    class MyObject {
        @objc dynamic func noArgsNoReturnFunc() {
        }
        @objc dynamic func sumFunc(a: Int, b: Int) -> Int {
            return a + b
        }
        @objc dynamic class func classMethodNoArgsNoReturnFunc() {
        }
    }
    

    #f03c15 The key words of methods @objc and dynamic are necessary

    #f03c15 The class doesn't have to inherit from NSObject. If the class is written by Objective-C, Just hook it without any more effort

    1. Perform the hook closure before executing specified instance's method.
    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
    
    1. Perform the hook closure after executing specified instance's method. And get the parameters.
    let object = MyObject()
    let token = try? hookAfter(object: object, selector: #selector(MyObject.sumFunc(a:b:)), closure: { a, b in
        // get the arguments of the function
        print("arg1 is \(a)") // arg1 is 3
        print("arg2 is \(b)") // arg2 is 4
        } as @convention(block) (Int, Int) -> Void)
    _ = object.sumFunc(a: 3, b: 4)
    token?.cancelHook() // cancel the hook
    

    #f03c15 The key word @convention(block) is necessary

    #f03c15 For hook at before and after. The closure's args have to be empty or the same as method. The return type has to be void

    1. Totally override the mehtod for specified instance. You can call original with the same parameters or different parameters. Don't even call the original method if you want.
    let object = MyObject()
    let token = try? hookInstead(object: object, selector: #selector(MyObject.sumFunc(a:b:)), closure: { original, a, b in
        // get the arguments of the function
        print("arg1 is \(a)") // arg1 is 3
        print("arg2 is \(b)") // arg2 is 4
    
        // run original function
        let result = original(a, b) // Or change the parameters: let result = original(-1, -2)
        print("original result is \(result)") // result = 7
        return 9
        } as @convention(block) ((Int, Int) -> Int, Int, Int) -> Int)
    let result = object.sumFunc(a: 3, b: 4) // result
    print("hooked result is \(result)") // result = 9
    token?.cancelHook() // cancel the hook
    

    #f03c15 For hook with instead. The closure's first argument has to be a closure which has the same types with the method. The rest args and return type have to be the same as the method.

    1. Perform the hook closure before executing the method of all instances of the class.
    let token = try? hookBefore(targetClass: MyObject.self, selector: #selector(MyObject.noArgsNoReturnFunc)) {
        // run your code
        print("hooked!")
    }
    MyObject().noArgsNoReturnFunc()
    token?.cancelHook() // cancel the hook
    
    1. Perform the hook closure before executing the class method.
    let token = try? hookClassMethodBefore(targetClass: MyObject.self, selector: #selector(MyObject.classMethodNoArgsNoReturnFunc)) {
        // run your code
        print("hooked!")
    }
    MyObject.classMethodNoArgsNoReturnFunc()
    token?.cancelHook() // cancel the hook