macOS应用配置共享菜单
macOS应用配置共享菜单

macOS应用配置共享菜单

macOS通过Finder右击文件,选择「共享」菜单,在菜单中显示自己的App,需要使用Share Extension(共享扩展)功能。

配置共享扩展

在Xcode菜单栏中选择 File → New → Target,找到“Share Extension”。

点击下一步,命名为某某共享。

需要注意的一点是,User Interface默认为Custom,表示只是从共享菜单触发App。如果选择 Compose Service View,则表示提供一个编辑内容,类似微博、邮件等编辑后再分享的App。

配置Info.plist

在新创建的Share Extension的Info.plist文件中,配置接受的文件类型。

默认接受所有类型的文件,如果需要单独限制(只共享图片),则需要配置Info.plist → NSExtension → NSExtensionAttributes → NSExtensionActivationRule,这个字段表示接受的文件类型。

NSExtensionActivationRule默认为字符串类型,内容为:

TRUEPREDICATE

这表示任何内容都可以显示在共享菜单中,不会区分文件还是图片等格式。

因为“轻压图片”只处理图片格式的文件,这里只接收图片文件的共享。

将NSExtensionActivationRule字符串类型,改为Dictionary(字典)类型。

常用参数分类:

1、NSExtensionActivationSupportsImageWithMaxCount:图片类型。

2、NSExtensionActivationSupportsFileWithMaxCount:文件类型。

3、NSExtensionActivationSupportsWebURLWithMaxCount:支持URL。

4、NSExtensionActivationSupportsText:支持纯文本。

5、NSExtensionActivationSupportsRichText:支持富文本。

6、NSExtensionActivationSupportsMovieWithMaxCount:支持视频。

7、NSExtensionActivationSupportsAudioWithMaxCount:支持音频。

后面的参数表示最多支持的数量,图片类型建议为10-100。

配置完成后,编译应用。

Finder中右击选择图片文件 → 共享 → 可以看到App。

如果没有显示,则需要到“编辑扩展”中将App改为显示状态。

本地化扩展名称

默认的扩展名称并不是本地化的。需要参考macOS的本地化流程,重新对共享菜单进行本地化。

如果修改本地化名称后,Finder没有同步修改过来,则重启访达:

killall Finder

或者注销一次用户。

共享扩展文件

1、删除默认的共享界面

Share Extension 默认会显示一个标准的共享界面。

解决方案:直接删除ShareViewController.xib文件并修改 ShareViewController 文件代码。

ShareViewController文件代码:

class ShareViewController: NSViewController {
    
    override var nibName: NSNib.Name? {
        return nil
    }
    
    override func loadView() {
        self.view = NSView(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
    }
    
    override func viewDidAppear() {
        super.viewDidAppear()
        // 调用共享代码
        processSharedContent()
    }
    
    private func processSharedContent() {
        print("调用 processSharedContent 方法")
        // 完成
        close()
    }
    
    private func close() {
        extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
    }
}

当选择文件并共享给 App 时,共享扩展会生创建一个透明视图,在调用完processSharedContent 代码后,调用close方法关闭扩展。

2、共享扩展数据

共享菜单属于扩展,和Watch类似,不能直接调用主 App 的代码。

目前主要有两种数据共享方式:通过NSExtensionItem + NSItemProvider 传递数据后,使用App Groups共享数据,最后通过URL Scheme(深层链接)打开应用并处理App Groups中的数据。

1、获取共享数据

private func processSharedContent() {
    // 第一步:从 extensionContext 获取共享的文件
    guard let extensionContext = self.extensionContext,
          let inputItems = extensionContext.inputItems as? [NSExtensionItem],
          let extensionItem = inputItems.first,
          let attachments = extensionItem.attachments else {
        print("没有共享内容")
        close()
        return
    }
    
    let fileProviders = attachments.filter {
        // 检测 provider 是否提供文件URL
        $0.hasItemConformingToTypeIdentifier("public.file-url")
    }
    
    let group = DispatchGroup()
    
    for itemProvider in fileProviders {
        group.enter()
        itemProvider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { [weak self] (secureCoding, error) in
            defer { group.leave() }
            
            DispatchQueue.main.async {
                if let url = secureCoding as? URL {
                    self?.handleImageFile(url)
                } else if let data = secureCoding as? Data,
                          let urlString = String(data: data, encoding: .utf8),
                          let url = URL(string: urlString) {
                    self?.handleImageFile(url)
                }
            }
        }
        
    }
    
    // 等待所有文件处理完毕
group.notify(queue: .main) { [weak self] in
    self?.openMainApp() // 使用深层链接打开主 App
        self?.close()
    }
}

当用户共享文件时,如果用户共享一个图片或者多张图片,inputItems数量为1,如果同时共享图片和文件,inputItems可能会>1。

用户共享三张图片的场景:

inputItems (数组长度: 1)
  └─ NSExtensionItem [0]
       └─ attachments (数组长度: 3)
            ├─ NSItemProvider [0] → 图片1.jpg
            ├─ NSItemProvider [1] → 图片2.jpg
            └─ NSItemProvider [2] → 图片3.jpg

用户共享1张图片和1个文本的场景:

inputItems (数组长度: 2)
  ├─ NSExtensionItem [0]
  │    └─ attachments (数组长度: 1)
  │         └─ NSItemProvider [0] → 图片.jpg
  └─ NSExtensionItem [1]
       └─ attachments (数组长度: 1)
            └─ NSItemProvider [0] → 文本内容

该代码仅适用于用户选择同一种类型附件的场景,如果用户选择多种不同类型的附件,则需要处理每一个NSExtensionItem。

2、启用 App Groups 共享数据

需要先配置 App Groups,主App和共享需要使用相同的 Group ID。

private func handleImageFile(_ url: URL) {
    // App Group 名称
    let appGroupIdentifier = "group.com.fangjunyu.ImageSlim"
    // 共享文件夹路径
    let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)!
    // 共享图片的临时目录
    let sharedDir = containerURL.appendingPathComponent("SharedImages", isDirectory: true)
    // 文件在共享目录中的路径(使用 UUID + 文件名的格式)
    let destinationURL = sharedDir.appendingPathComponent(UUID().uuidString + "_" + url.lastPathComponent)

     // 尝试创建临时目录(如果不存在)
    try? FileManager.default.createDirectory(at: sharedDir, withIntermediateDirectories: true)

    // 将文件写入目标目录
    do {
        try FileManager.default.copyItem(at: url, to: destinationURL)
        print("文件已保存到共享目录: \(destinationURL.path)")
    } catch {
        print("文件 \(url) 写入失败: \(error.localizedDescription)")
    }
}

注意:Xcode 16 在这里访问文件URL时,并没有要求安全作用域,如果后续的版本需要安全作用域,则需要在复制文件代码前添加:

let isAccessing = url.startAccessingSecurityScopedResource()
defer {
    if isAccessing {
        url.stopAccessingSecurityScopedResource()
    }
}

安全作用域的代码可以参考安全书签中的使用方式。

3、通过URL Scheme(深层链接)打开主 App

需要先给主App配置URL Scheme,在获取共享数据的最后代码中,处理完全URL后,调用openMainApp方法,打开主App。

private func openMainApp() {
    // URL Scheme,需要在主 App 的 Info.plist 中配置
    let urlScheme = "imageslim://open-shared-images"
    
    if let url = URL(string: urlScheme) {
        // macOS 使用 NSWorkspace 打开 URL
        NSWorkspace.shared.open(url)
    }
}

4、主 App 接收URL Scheme

主 App 主要通过两种方式接收 URL Scheme,分别是AppDelegate或者onOpenURL修饰符接收数据。

1)AppDelegate接收URL Scheme

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
    // 接收打开的图片文件
    func application(_ application: NSApplication, open urls: [URL]) {
        print("通过 application(_:open:) 接收到 URL")
        for url in urls {
            // URL Scheme 分发的事件,例如:ImageSlim://open-shared-images
            if url.scheme == "ImageSlim", url.host == "open-shared-images" {
                print("URL Scheme分发的事件")
                retrieveSharedImageURLs()
            } else {
                print("其他分发的事件")
            }
        }
    }
}

2)onOpenURL接收URL Scheme

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    if url.scheme == "ImageSlim", url.host == "open-shared-images" {
                        print("URL Scheme分发的事件")
                        retrieveSharedImageURLs()
                    } else {
                        print("其他分发的事件")
                    }
                }
        }
    }
}

注意:onOpenURL每次只传一个URL,如果系统传入多个URL,SwiftUIn诶不把URL数组拆分,逐条派发执行。

5、主 App 处理共享文件夹

func retrieveSharedImageURLs() {
    
    // App Group ID
    let appGroupIdentifier = "group.com.fangjunyu.ImageSlim"
    // 共享文件夹路径
    let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)!
    // 共享文件夹的临时目录
    let sharedDir = containerURL.appendingPathComponent("SharedImages", isDirectory: true)
    
    // 检查目录是否存在
    guard FileManager.default.fileExists(atPath: sharedDir.path) else {
        print("共享目录不存在")
        return
    }
    
    // 共享文件夹中的文件URL
    var imagesURLs:[URL] = []
    do {
        // 尝试读取共享目录中的文件
        let fileURLs = try FileManager.default.contentsOfDirectory(
            at: sharedDir,
            includingPropertiesForKeys: nil,
            options: .skipsHiddenFiles
        )
        imagesURLs = fileURLs.filter { url in
            let ext = url.pathExtension.lowercased()
            return ["jpg", "jpeg", "png", "tif", "tiff", "gif", "bmp", "webp", "heic", "heif", "jp2", "j2k", "jpf", "jpx", "jpm", "pdf"].contains(ext)
        }
    } catch {
        print("读取共享目录失败: \(error)")
        return
    }
    
    // 处理图片文件的代码
    
    // 最后,清理共享目录
    do {
        let fileURLs = try FileManager.default.contentsOfDirectory(
            at: sharedDir,
            includingPropertiesForKeys: nil
        )
        
        for fileURL in fileURLs {
            try FileManager.default.removeItem(at: fileURL)
        }
        print("共享目录已清理")
    } catch {
        print("清理共享目录失败: \(error)")
    }
}

和共享扩展步骤一致,先获取App Groups共享文件夹的路径,使用FileManager.default.contentsOfDirectory尝试获取并筛选共享文件夹中的文件。

在执行完全部代码后,清理共享目录中的文件,防止重复处理。

注意事项

在测试共享扩展时,会要求选择打开的应用。

Xcode编译的应用默认存在 DerivedData文件夹下:

/Users/fangjunyu/Library/Developer/Xcode/DerivedData/

需要将编译后的应用,从DerivedData中复制到应用程序文件夹。

步骤如下:

1、打开 Finder ,按 Command + Shift + G,弹出路径输出框。

2、资源库中Xcode的DerivedData文件夹路径(可参考上面的路径)。

3、打开DerivedData文件夹,找到Xcode项目 → Build → Products → Debug → 应用包。

4、将应用包复制到应用程序目录中。

5、Xcode编译时,选择最新编译的应用包。

如果出现一些崩溃的问题,通常可能是因为不是Xcode调试的应用版本。

NSNib _initWithNibNamed: "ShareViewController"

总结

macOS应用如果配置共享菜单,需要新增共享扩展,并通过URL Scheme或者AppGroup等方式实现数据的传递。

相关文章

1、macOS应用配置文件的可打开应用

2、macOS本地化应用程序名称

3、SwiftUI配置深层链接

4、SwiftUI UserDefaults使用App Group共享数据

5、Apple NSItemProvider类

6、macOS安全书签(Security Scoped Bookmark)

7、SwiftUI打开事件修饰符onOpenURL

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注