mhdhejazi / Dynamic

Call hidden/private API in style! The Swift way.

Home Page:https://medium.com/@mhdhejazi/calling-ios-and-macos-hidden-api-in-style-1a924f244ad1

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Crashed when completionBlock passed as argument

SpectralDragon opened this issue · comments

I wanna call method beginSheetModalForWindow:completionHandler: in NSOpenPanel object, but app crashed in Invocation.swift file at line 139.

How to pass object?

It would be great if you post a complete example so I can reproduce the issue.

extension UIWindow {
   // Works correctly =)
    var nsWindow: AnyObject? {
        guard let nsWindows = NSClassFromString("NSApplication")?.value(forKeyPath: "sharedApplication.windows") as? [AnyObject] else { return nil }
        for nsWindow in nsWindows {
            let uiWindows = nsWindow.value(forKeyPath: "uiWindows") as? [UIWindow] ?? []
            if uiWindows.contains(self) { return nsWindow }
        }
        return nil
    }
}

// button action

let panel = Dynamic.NSOpenPanel()
panel.beginSheetModalForWindow(self.view.window!.nsWindow!, completionHandler: { response in
    print(response)
})

I see.

The problem here is that NSOpenPanel.beginSheetModalForWindow() expects an Objective-C block, and we're passing a Switch closure which is entirely a different thing, and not interchangeable with Objective-C blocks.

From docs:

Block objects are a C-level syntactic and runtime feature.

And from this post:

(...) closures are a super-special case, like a custom type, built in Swift. Where in Objective-C, all of them are Objective-C objects. So to bridge Swift closure into Objective-C, Swift needs to create an Objective-C object for you

I can think of two options here:

  1. Use panel.runModal(), and implement NSOpenSavePanelDelegate.validateURL to know when the user presses the "Open" button.
  2. Create an Objective-C class that calls the method for you, and Swift will handle the bridging between ObjC blocks and Swift closures.

BTW, the extension can be re-written as following:

extension UIWindow {
    var nsWindow: NSObject? {
        Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(self)
    }
}

Good news! I found a way to make Swift convert the closure to an Objective-C block.

You just need to add @convention(block) to the closure type:

typealias ResultBlock = @convention(block) (_ result: Int) -> Void

Now, this works flawlessly:

let panel = Dynamic.NSOpenPanel()
panel.beginSheetModalForWindow(self.view.window.nsWindow, completionHandler: { result in
    print("result: ", result)
} as ResultBlock)

I added a section about Objective-C blocks in the readme file. Thank you for bringing this to my attention!