如何在 Swift 中实现 NSDocument 方法 -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:?

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

在我的应用程序中,一个 NSDocument 子类关键任务硬件 – 用户真的不希望意外关闭文档!所以,我实现了 canCloseDocumentWithDelegate… 以显示 NSAlert 并在关闭前询问。

我现在正尝试在用 Swift 编写的应用程序中实现同样的事情。

由于答案是异步的,“应该关闭”的结果被传递给委托的回调,而不是简单地返回。在 -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo: 的文档中,它说:

The shouldCloseSelector callback method should have the following signature:

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

所以,因为有 3 个不同类型的参数,我不能使用简单的 performSelector:withObject: 风格的方法——你必须使用 NSInvocation。注意delegate是id类型,上面的签名没有出现在任何正式的协议中——不能简单的正常调用方法。 (请参阅此 mailing list post 以了解如何完成此操作的示例)

现在,问题是 Swift 中不允许使用 NSInvocation!请参阅 Swift 博客 “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.

这听起来是件好事,但当一个简单的 NSDocument API 仍然需要 NSInvocation 时,它就失败了!整个问题的真正解决方案是 Apple 使用块回调引入新的 canCloseDocument… API。但在此之前,最好的解决方案是什么?

因此,我目前的解决方案是继续使用 Objective-C 来执行 NSInvocation。 NSDocument 子类在 Swift 中编写,并调用一个 Objective-C 类别来完成这项工作。

既然NSInvocation在Swift中不存在,我真的看不出有什么别的办法。

- (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];
}

你可以看到我的示例项目: https://github.com/DouglasHeriot/canCloseDocumentWithDelegate

另一种选择是使用Objective-C环绕objc_msgSend,这在Swift中也不可用。 http://www.cocoabuilder.com/archive/cocoa/87293-how-does-canclosedocumentwithdelegate-work.html#87295

您可以使用一些低级运行时函数解决此问题:

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)
}

如果您需要将此行为移动到另一个方法(例如,在 sheet 获得用户确认后),只需将委托和 shouldCloseSelector 存储在属性中,以便稍后访问它们。

这是我从 Apple Developer Technical Support 收到的针对此问题的 Swift 解决方案:

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()
    }
}

至少从 Swift 4.1 开始,您可以执行以下操作:

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

...

// Handler
@objc
private func document(_ doc: NSDocument, _ shouldClose: Bool, _ contextInfo: UnsafeMutableRawPointer) {
    ...
}