如何在macOS应用程序中从app注册服务?

Mil*_*sáľ 4 service xcode swift3 macos-sierra

我正在尝试实现一个上下文菜单项,在选择文本时将在服务中显示.例如,如果我在TextEdit中选择一个单词,我希望在上下文菜单中显示一个菜单项"Do my stuff",这将把所选单词提供给我的应用程序代码以供进一步处理.

通过谷歌搜索,我得出结论,我需要实现和注册服务.我试图遵循 提供服务 文档,但这似乎至少有点过时(更不用说在某些地方含糊不清).

根据我的理解,我能够做到以下几点:

我实现了服务对象:

import Foundation
import AppKit

class ContextualMenuServiceProvider {

    func importString(_ pasteBoard: Pasteboard, userData: String, error: String) {
         print(">>>> in import string from service consumer")
    }
}
Run Code Online (Sandbox Code Playgroud)

我在Info.plist中为服务创建了一个条目:

    <key>NSServices</key>
    <array>
        <dict>
            <key>NSMenuItem</key>
            <dict>
                <key>default</key>
                <string>Import to $(PRODUCT_NAME)</string>
            </dict>
            <key>NSMessage</key>
            <string>importString</string>
            <key>NSPortName</key>
            <string>MyApp</string>
            <key>NSSendTypes</key>
            <array>
                <string>public.plain-text</string>
            </array>
        </dict>
    </array>
Run Code Online (Sandbox Code Playgroud)

最后,我尝试在AppDelegate中注册该服务.现在文档说要使用:

NSApp.setServicesProvider(encryptor)
// where encryptor is an object of my ContextualMenuServiceProvider
Run Code Online (Sandbox Code Playgroud)

但是,似乎NSApp没有setServicesProvider方法.我试着用:

 NSApp.servicesProvider = ContextualMenuServiceProvider()
Run Code Online (Sandbox Code Playgroud)

但是,这似乎也不起作用.

我通过从Xcode运行应用程序来测试它,然后,在应用程序运行时,我尝试在TextEdit中选择一些文本(它不应该仅限于TextEdit,我只是以它为例),然后右键单击我正在寻找我的菜单项目.这是行不通的.

我能够找到某种类似的SO问题(例如,创建一个os x服务,或者如何将菜单项添加到Delphi XE2中的Mac OS Finder),但是我无法从他们那里检测出我做错了什么(不是提到他们中的大多数是旧的(使用setServiceProvider),或使用像C#或Delphi这样的语言.

知道我的代码/方法有什么问题吗?

Mil*_*sáľ 6

好吧,看起来我犯了几个小错误,让我相信它不起作用.但是,最终我能够让它发挥作用.因此,如果您面临同样的任务,这里是Swift 3中的解决方案:

(1)您必须实现一种服务方法:

import Cocoa

class ServiceProvider: NSObject {

    let errorMessage = NSString(string: "Could not find the text for parsing.")

    @objc func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>) {
        guard let str = pasteboard.string(forType: NSStringPboardType) else {
            error.pointee = errorMessage
            return
        }

        let alert = NSAlert()
        alert.messageText = "Hello \(str)"
        alert.informativeText = "Welcome in the service"
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }
}
Run Code Online (Sandbox Code Playgroud)

以前我不知道的重要事情是提供服务的对象必须是NSObject的子类(它也可以是ViewController,或任何其他NSObject子类).Services API使用selector来调用它,而selector技术只能与NSObject一起使用.

此外,service方法必须具有此声明:它需要3个参数,首先是NSPasteboard(您可以使用Pasteboard,但不提供string方法)未标记,第二个是可选String标记userData,第三个AutoreleasingUnsafeMutablePointer<NSString>标记error.按照这一点来获得最佳的类型安全性,同时仍然使其工作.否则,服务API将无法找到它并调用它.你可以用它UnsafeRawPointers的所有参数,但你不会得到任何东西.

(2)在app delegate(或文档说你可以在任何地方进行,但我在这里做)你注册服务提供者:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        NSApplication.shared().servicesProvider = ServiceProvider()

        NSUpdateDynamicServices()
    }

}
Run Code Online (Sandbox Code Playgroud)

它似乎也在没有NSUpdateDynamicServices通话的情况下工作,但我想更安全而不是抱歉 - 它应该刷新系统中的服务,这样你就不必注销并登录用户来获取当前的服务版本.

(3)最后,您必须配置Info.plist以宣传您的服务方法:

<key>NSServices</key>
<array>
    <dict>
        <key>NSMessage</key>
        <string>service</string>

        <key>NSPortName</key>
        <string>serviceTest</string>

        <key>NSMenuItem</key>
        <dict>
            <key>default</key>
            <string>Test hello world</string>
        </dict>

        <key>NSRestricted</key>
        <false/>

        <key>NSRequiredContext</key>
        <dict/>

        <key>NSSendTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
    </dict>
</array>
Run Code Online (Sandbox Code Playgroud)

这是Info.plist的XML源代码 - 虽然Apple建议使用他们的编辑器,但在Xcode 8.2中我无法NSRequiredContext通过编辑器添加密钥 - 我必须将文件作为源代码打开并手动添加.我建议直接编辑XML源代码.

您可以在服务属性中找到有关每个键含义的文档,但我想提一些关键点.首先,你需要NSRequiredContext密钥 - 他们在文档中提到它,但编辑器不支持它 - 直接编辑XML并将其添加为空(就像我一样).其次,如果您正在使用NSSendTypes或者NSReturnTypes您希望使用字符串,请使用NSStringPboardType即使其文档说它已被弃用并应替换为NSPasteboardTypeString- 后者将无效.最后,NSMessagekey with servicevalue是服务方法的名称.所以我的服务提供者对象声明了以下方法:

func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>)
Run Code Online (Sandbox Code Playgroud)

我正在使用该serviceNSMessage.如果你想改变它(例如,你想要几种服务方法),不要忘记这两者必须匹配(Info.plist配置中的值和服务方法的名称).

(4)在这种状态下应该有效.有一件事我想提一下,可能会帮助你进行调试.最后通过运行以下文档中提到的方法测试它:

 /Applications/TextEdit.app/Contents/MacOS/TextEdit -NSDebugServices com.mycompany.myapp
Run Code Online (Sandbox Code Playgroud)

从终端运行此应该报告是否已注册使用提供的包的任何服务,以及是否将其呈现 - 例如,在我的情况下,问题是我没有提供强制性NSRequiredContext.在使用这种方法测试之后,我能够识别出服务已经安装,问题是服务API认为它应该被过滤掉.经过一些实验和谷歌搜索,我通过添加空来解决它NSRequiredContext.

每次更改服务后,我建议退出TextEdit并再次运行它,似乎它保留了对旧服务提供者对象的引用(或类似的东西,如果我没有重新启动它,TextEdit没有注册更改).