You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

关键修改说明

  1. 同步收集逻辑:用DispatchGroup包裹所有文本和图片的加载操作,确保所有资源都加载完成后再处理UI,避免因异步操作导致的状态不一致。
  2. 遍历所有Attachments:不再只处理第一个Attachment,而是遍历所有ExtensionItem下的所有Attachment,确保不会遗漏任何文本或图片资源。
  3. 安全的错误处理:用defer { group.leave() }确保无论加载成功还是失败,都能正确退出DispatchGroup,防止出现死锁导致扩展挂起。
  4. 支持三种场景:统一收集文本和图片后,自然支持“图文/纯文本/纯图片”三种分享场景,无需分支判断。
  5. 图片存储扩展:增加了图片转Base64存储到共享容器的逻辑,方便主App读取;同时修改了ShareExtensionView来展示图片。

额外注意事项

  • 如果需要对图片做OCR识别,记得把OCR操作也加入DispatchGroup,确保识别完成后再合并文本结果。
  • 共享容器的权限要配置正确,确保主App和扩展能正常读写。
  • 测试时记得用真实设备,模拟器的分享扩展有时候会有兼容性问题。

备注:内容来源于stack exchange,提问作者Tanvirgeek

火山引擎 最新活动