import UIKit import UniformTypeIdentifiers // Share Extension 主控制器:把 share sheet 传入的文件拷进 App Group 收件箱,再深链唤起主 app // 选设备发送(扩展只交接、不跑引擎,避 120MB jetsam,见 PLAN §I5)。无 compose UI——即起即办、 // 处理完即返回。 final class ShareViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Task { await processAndClose() } } private func processAndClose() async { let items = (extensionContext?.inputItems as? [NSExtensionItem]) ?? [] var staged = 0 for item in items { for provider in item.attachments ?? [] { if await stage(provider) { staged += 1 } } } let ctx = extensionContext guard staged > 0, let url = URL(string: "cdrop://share") else { ctx?.completeRequest(returningItems: nil) return } // 先唤起主 app,等 open 回调拿到结果再 completeRequest——否则立即 completeRequest 会拆掉 // 扩展、self 被释放,open 失败时的 openViaResponder 兜底永不触发(审查 L8)。捕获 ctx 使 // 扩展存活到回调。 ctx?.open(url) { [weak self] ok in if !ok { self?.openViaResponder(url) } ctx?.completeRequest(returningItems: nil) } } // 把单个附件落到 App Group 收件箱。用 loadFileRepresentation 取系统给的临时文件表示 // (文件 / 图片 / 文档统一走此路,且大文件不整体进内存),在回调内同步拷到收件箱 // (系统会在回调返回后删临时文件)。 private func stage(_ provider: NSItemProvider) async -> Bool { return await withCheckedContinuation { cont in provider.loadFileRepresentation(forTypeIdentifier: UTType.item.identifier) { url, _ in guard let url, let dest = AppGroup.stagedDestination(originalName: url.lastPathComponent) else { cont.resume(returning: false); return } do { try FileManager.default.copyItem(at: url, to: dest) cont.resume(returning: true) } catch { cont.resume(returning: false) } } } } // open 失败时的兜底:沿响应链找 UIApplication 唤起主 app(个别系统版本扩展直接 open 不生效)。 private func openViaResponder(_ url: URL) { var responder: UIResponder? = self let sel = NSSelectorFromString("openURL:") while let r = responder { if r.responds(to: sel) { _ = r.perform(sel, with: url); return } responder = r.next } } }