iOS备忘录分享扩展同时采集文本和图片的功能实现求助
iOS备忘录分享扩展同时采集文本和图片的功能实现求助
嗨,我看了你遇到的问题——当从iOS备忘录分享文本+图片组合内容时,分享扩展直接挂掉,但只传单一类型(纯文本/纯图片)时正常,这确实挺让人头疼的。咱们来拆解问题根源,然后一步步解决它:
问题根源分析
你当前的代码逻辑是分支处理:先检查第一个ExtensionItem的第一个Attachment是不是纯文本,如果是就只处理文本;否则才遍历所有Item处理图片。但备忘录在分享图文混合内容时,同一个ExtensionItem里会同时包含文本和图片的Attachment,你的代码只处理了文本就终止了逻辑,完全没处理图片,而且没有等待所有资源加载完成就操作UI,导致资源占用异常,最终引发扩展崩溃。
解决方案:同步收集所有文本和图片
我们需要重构逻辑,改为同时收集所有文本和图片资源,用DispatchGroup等待所有加载操作完成后,再统一处理这些内容。下面是修改后的完整代码和关键说明:
修改后的完整代码
import UIKit import SwiftUI import Vision import UniformTypeIdentifiers class ShareViewController: UIViewController { // 用来存储收集到的文本和图片 private var collectedText: String = "" private var collectedImages: [UIImage] = [] override func viewDidLoad() { super.viewDidLoad() guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem], !extensionItems.isEmpty else { close() return } let group = DispatchGroup() for item in extensionItems { guard let attachments = item.attachments, !attachments.isEmpty else { continue } for provider in attachments { // 处理文本类型 if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { group.enter() provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (result, error) in defer { group.leave() } // 确保无论成功失败都离开group if let error = error { print("加载文本失败: \(error.localizedDescription)") return } if let text = result as? String { self?.collectedText = text } } } // 处理图片类型 if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { group.enter() provider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (result, error) in defer { group.leave() } // 确保无论成功失败都离开group if let error = error { print("加载图片失败: \(error.localizedDescription)") return } guard let resultURL = result as? NSURL, let url = resultURL as URL? else { print("无效的图片URL") return } do { let imageData = try Data(contentsOf: url) if let image = UIImage(data: imageData) { self?.collectedImages.append(image) // 如果需要对图片做OCR,在这里调用processImageForText,注意要把结果合并到collectedText // self?.processImageForText(image, group: group) } } catch { print("读取图片数据失败: \(error.localizedDescription)") } } } } } // 所有资源加载完成后,统一处理UI group.notify(queue: .main) { [weak self] in guard let self = self else { return } // 处理三种情况:图文/纯文本/纯图片 var displayText = self.collectedText // 如果有图片且需要OCR,可以在这里合并OCR结果到displayText // 比如遍历collectedImages,调用OCR后拼接文本 // 传递数据给ShareView,这里可以修改ShareView来接收图片数组 self.displayShareView(with: displayText, images: self.collectedImages) } } // 可选:如果需要对图片做OCR,修改这个方法支持DispatchGroup private func processImageForText(_ image: UIImage, group: DispatchGroup) { group.enter() DispatchQueue.global(qos: .userInitiated).async { [weak self] in defer { group.leave() } guard let cgImage = image.cgImage else { print("无法将UIImage转为CGImage") return } let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:]) let request = VNRecognizeTextRequest { [weak self] (request, error) in if let error = error { print("OCR识别失败: \(error.localizedDescription)") return } let recognizedText = request.results?.compactMap { result in (result as? VNRecognizedTextObservation)?.topCandidates(1).first?.string }.joined(separator: "\n") ?? "" if !recognizedText.isEmpty { self?.collectedText += "\n\(recognizedText)" } } do { try requestHandler.perform([request]) } catch { print("执行OCR请求失败: \(error.localizedDescription)") } } } // 修改displayShareView来支持接收图片数组 private func displayShareView(with text: String, images: [UIImage]) { // 这里需要修改ShareExtensionView,让它支持接收images参数 let contentView = UIHostingController(rootView: ShareExtensionView(text: text, images: images, onSave: { [weak self] savedText, title in self?.handleSaveAction(with: savedText, title: title, images: images) })) self.addChild(contentView) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor), contentView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), contentView.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), contentView.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) ]) contentView.didMove(toParent: self) } // 修改handleSaveAction来处理图片存储 private func handleSaveAction(with text: String, title: String, images: [UIImage]) { let userDefaults = UserDefaults(suiteName: "group.aibuddy.aibuddyshareextention") // 存储文本 userDefaults?.set(text, forKey: "sharedTextFromExtensionText") userDefaults?.set(title, forKey: "sharedTextFromExtensionTitle") // 存储图片:可以把图片转成base64字符串存储,或者保存到共享容器的文件里 let imageBase64Array = images.compactMap { image in image.pngData()?.base64EncodedString() } userDefaults?.set(imageBase64Array, forKey: "sharedImagesFromExtension") userDefaults?.synchronize() // 打开主App的逻辑保持不变 let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let encodedText = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let urlScheme = "aibuddyshare://openFromShareExtension?title=\(encodedTitle)&text=\(encodedText)" if let url = URL(string: urlScheme) { print("尝试打开主App: \(url)") let _ = openURL(url) } else { print("无效的URL Scheme") } close() } func close() { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } @objc func openURL(_ url: URL) -> Bool { var responder: UIResponder? = self while responder != nil { if let application = responder as? UIApplication { return application.perform(#selector(openURL(_:)), with: url) != nil } responder = responder?.next } return false } } // 示例:修改ShareExtensionView来支持接收图片 struct ShareExtensionView: View { let text: String let images: [UIImage] let onSave: (String, String) -> Void var body: some View { VStack { if !text.isEmpty { Text(text) .padding() } ScrollView { ForEach(images.indices, id: \.self) { index in Image(uiImage: images[index]) .resizable() .scaledToFit() .padding() } } Button("保存到主App") { onSave(text, "备忘录分享内容") } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } } }
关键修改说明
- 同步收集逻辑:用
DispatchGroup包裹所有文本和图片的加载操作,确保所有资源都加载完成后再处理UI,避免因异步操作导致的状态不一致。 - 遍历所有Attachments:不再只处理第一个Attachment,而是遍历所有ExtensionItem下的所有Attachment,确保不会遗漏任何文本或图片资源。
- 安全的错误处理:用
defer { group.leave() }确保无论加载成功还是失败,都能正确退出DispatchGroup,防止出现死锁导致扩展挂起。 - 支持三种场景:统一收集文本和图片后,自然支持“图文/纯文本/纯图片”三种分享场景,无需分支判断。
- 图片存储扩展:增加了图片转Base64存储到共享容器的逻辑,方便主App读取;同时修改了
ShareExtensionView来展示图片。
额外注意事项
- 如果需要对图片做OCR识别,记得把OCR操作也加入
DispatchGroup,确保识别完成后再合并文本结果。 - 共享容器的权限要配置正确,确保主App和扩展能正常读写。
- 测试时记得用真实设备,模拟器的分享扩展有时候会有兼容性问题。
备注:内容来源于stack exchange,提问作者Tanvirgeek




