How can you implement the NSDocument method -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo: in Swift?
Asked Answered
K

4

7

In my application, a NSDocument subclass mission-critical hardware – users really don’t want to close a document by accident! So, I’ve implemented canCloseDocumentWithDelegate… to show an NSAlert and ask before closing.

I am now trying to implement this same thing in an application written in Swift.

Since the answer comes asynchronously, the “should close” result is passed to a callback on a delegate, and not simply returned. In the documentation for -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:, it says:

The shouldCloseSelector callback method should have the following signature:

- (void)document:(NSDocument *)doc shouldClose:(BOOL)shouldClose contextInfo:(void *)contextInfo

So, as there’s 3 arguments of different types, I cannot use the simple performSelector:withObject: style methods – you have to use NSInvocation. Note that the delegate is of type id, and the signature above does not appear in any formal protocol – you can’t simply call the method normally. (See this mailing list post for example of how this should be done)

Now, the issue is, NSInvocation is not allowed in Swift! See Swift blog “What Happened to NSMethodSignature”:

Bringing the Cocoa frameworks to Swift gave us a unique opportunity to look at our APIs with a fresh perspective. We found classes that we didn't feel fit with the goals of Swift, most often due to the priority we give to safety. For instance, some classes related to dynamic method invocation are not exposed in Swift, namely NSInvocation and NSMethodSignature.

That sounds like a good thing, but falls down when a simple NSDocument API requires NSInvocation still! The real solution to this whole problem would be for Apple to introduce a new canCloseDocument… API using a block callback. But until that happens, what’s the best solution?

Kreutzer answered 1/12, 2015 at 23:15 Comment(0)
S
7

You can solve this with some low level runtime functions:

override func canCloseDocumentWithDelegate(delegate: AnyObject, shouldCloseSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {

    let allowed = true // ...or false. Add your logic here.

    let Class: AnyClass = object_getClass(delegate)
    let method = class_getMethodImplementation(Class, shouldCloseSelector)

    typealias signature = @convention(c) (AnyObject, Selector, AnyObject, Bool, UnsafeMutablePointer<Void>) -> Void
    let function = unsafeBitCast(method, signature.self)

    function(delegate, shouldCloseSelector, self, allowed, contextInfo)
}

If you need to move this behaviour to another method (eg. after a sheet gets confirmation from the user), simply store the delegate and shouldCloseSelector in properties so you can access them later.

Steerage answered 2/2, 2016 at 23:36 Comment(2)
I can confirm this works! Here’s my example code, now using this pure Swift implementation: github.com/DouglasHeriot/canCloseDocumentWithDelegate/commit/…Kreutzer
For those of you who haven't used Obj-C in years and are wondering about the extra parameters in the function() call, keep in mind that the receiving object and selector are needed for all Obj-C method invocations: self, then _cmd, then the visible method arguments.Cantabrigian
K
2

So, my current solution to this, is to keep using Objective-C to perform the NSInvocation. The NSDocument subclass is written in Swift, and calls an Objective-C category to do this bit of work.

Since NSInvocation does not exist in Swift, I really don’t see any other way.

- (void)respondToCanClose:(BOOL)shouldClose delegate:(id)delegate selector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo
{
    NSDocument *doc = self;

    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:shouldCloseSelector]];
    invocation.target = delegate;
    invocation.selector = shouldCloseSelector;
    [invocation setArgument:&doc atIndex:2]; // Note index starts from 2 - 0 & 1 are self & selector
    [invocation setArgument:&shouldClose atIndex:3];
    [invocation setArgument:&contextInfo atIndex:4];

    [invocation invoke];
}

You can see my sample project: https://github.com/DouglasHeriot/canCloseDocumentWithDelegate

Another option is to use Objective-C to wrap around objc_msgSend, which is also unavailable in Swift. http://www.cocoabuilder.com/archive/cocoa/87293-how-does-canclosedocumentwithdelegate-work.html#87295

Kreutzer answered 1/12, 2015 at 23:15 Comment(1)
I hope someone can come up with a better solution, but I doubt it. I filed a bug with Apple rdar://23714588. Prior to this answer, when Googling "canCloseDocumentWithDelegate" "Swift" there were only about 20 results, and none relevant. So, hopefully this will help someone stuck in the future.Kreutzer
F
0

At least as of Swift 4.1, you can do something like:

// Application Logic
myDocument.canClose(
    withDelegate: self,
    shouldClose: #selector(MyClass.document(_:_:_:)),
    contextInfo: nil)

...

// Handler
@objc
private func document(_ doc: NSDocument, _ shouldClose: Bool, _ contextInfo: UnsafeMutableRawPointer) {
    ...
}
Frug answered 24/9, 2018 at 2:13 Comment(0)
A
-1

Here is a Swift solution to this issue that I received from Apple Developer Technical Support:

override func canCloseDocumentWithDelegate(delegate: AnyObject, shouldCloseSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {
    super.canCloseDocumentWithDelegate(self, shouldCloseSelector: "document:shouldClose:contextInfo:", contextInfo: contextInfo)
}

func document(doc:NSDocument, shouldClose:Bool, contextInfo:UnsafeMutablePointer<Void>) {
    if shouldClose {
        // <Your clean-up code>
        doc.close()
    }
}
Aleris answered 29/3, 2016 at 21:11 Comment(4)
Can you please explain how this would help solve the issue?Particularity
You can now see this in use in the apple-dts-solution branch of my repo. github.com/DouglasHeriot/canCloseDocumentWithDelegate/blob/…Kreutzer
I’m not sure how much I like it – feels dirty to replace the delegate with self and then just do your own thing. But it seems to work, and is less code. I think I might keep using the @Steerage class_getMethodImplementation solution though.Kreutzer
It looks like there will be issues if the user is trying to quit the app instead of just closing the document. It is my understanding that the original selector would continue the quitting process once all documents are closed. If the the selector is ignored (never called), the app would continue to run.Endpaper

© 2022 - 2024 — McMap. All rights reserved.