1

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()
Faustino
  • 13
  • 5

1 Answers1

4

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 {
...
MANIAK_dobrii
  • 6,014
  • 3
  • 28
  • 55
  • Thanks! Only issue is now `log_initWithString` is seemingly catching all `init`s, with mostly all empty strings, is there a fix for that? – Faustino Nov 18 '21 at 19:37
  • 3
    @Faustino It might be called by other initializers, but most likely it might be from other call sites, since swizzling replaces method for ALL instances of the class. I'd suggest to never use swizzling, unless there is completely no other way (this was also true in the times of Objective-C). With tools as powerful as swizzling it is quite important to have full understanding of the internals. – MANIAK_dobrii Nov 18 '21 at 19:44
  • That makes sense, I'll be careful. Thanks again. – Faustino Nov 18 '21 at 19:51
  • You can also use `@objc(log_initWithString) internal func internal func log_initWithString(string URLString: String)` to explicitly set the objective c name as `log_initWithString` – Alexander Nov 18 '21 at 20:25