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等方式实现数据的传递。
相关文章
4、SwiftUI UserDefaults使用App Group共享数据
