Home > Software engineering >  Method Swizzling with Swift 5.5
Method Swizzling with Swift 5.5

Time:11-19

I'm trying to overwrite the initWithString method on NSURL, I've looked at these past issues/posts.

I've tried the following code but I haven't been able to pin the new log_initWithString method, swiftc doesn't flag anything but on run I'm getting index/index.swift:20: Fatal error: Unexpectedly found nil while unwrapping an Optional value.

import AppKit
import WebKit

let app = NSApplication.shared

//example: https://github.com/kickstarter/ios-oss/blob/39edeeaefb5cfb26276112e0af5eb6948865cf34/Library/DataSource/UIView-Extensions.swift

private var hasSwizzled = false

extension NSURL {
    public final class func doSwizzle() {
        guard !hasSwizzled else { return }

        hasSwizzled = true
        
        let original = Selector("initWithString:")
        let swizzled = Selector("log_initWithString:")

        let originalMethod = class_getInstanceMethod(NSURL.self, original)!
        let swizzledMethod = class_getInstanceMethod(NSURL.self, swizzled)!

        let didAddMethod = class_addMethod(NSURL.self, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

        if didAddMethod {
            class_replaceMethod(NSURL.self, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
    
    @objc internal func log_initWithString(string URLString: String) {
        NSLog("Hello from initWithString")
        
        return log_initWithString(string: URLString)
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    let window = NSWindow.init(contentRect: NSRect(x: 0, y: 0, width: 750, height: 600), styleMask: [
        NSWindow.StyleMask.titled,
        NSWindow.StyleMask.closable,
        NSWindow.StyleMask.resizable,
        NSWindow.StyleMask.miniaturizable
    ], backing: NSWindow.BackingStoreType.buffered, defer: false)
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        NSURL.doSwizzle()
        
        let webview = WKWebView(frame: window.contentView!.frame)
        let request = URLRequest(url: URL(string: "https://google.com")!)

        window.contentView?.addSubview(webview)
        webview.load(request)
        
        window.makeKeyAndOrderFront(nil)
        window.orderFrontRegardless()
        window.center()
    }
}

let delegate = AppDelegate()

app.delegate = delegate

app.run()

CodePudding user response:

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