You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift分享扩展在Google News中分享时崩溃问题排查

问题描述

我开发的Swift分享扩展支持接收URL和文本内容,在Mail、Safari、NetNewsWire等应用中均可正常工作,但在iOS 17.2及更高版本的Google News中分享时会出现异常:扩展视图无法完全渲染,仅显示半透明背景,且会陷入卡死状态,必须强制关闭Google News才能恢复。

调试过程中遇到的问题:

  • 添加Logger语句后无日志生成
  • 实体设备上无法通过Xcode调试扩展
  • iOS模拟器无法安装Google News,无法复现场景

即使在Safari等正常工作的场景中,扩展也会打印一条错误日志:

-[_EXSinkLoadOperator loadItemForTypeIdentifier:completionHandler:expectedValueClass:options:] nil expectedValueClass allowing {(
    _EXItemProviderSandboxedResource,
    NSData,
    ...
)}

相关代码

分享扩展视图控制器

class ShareViewController: UIViewController {    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard
            let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
            let itemProvider = extensionItem.attachments?.first
        else {
            self.close()
            return
        }
        
        if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
            itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { urlCoded, error in
                if let error {
                    self.close()
                    return
                }
                
                if let url = urlCoded as? URL {
                    DispatchQueue.main.async {
                        self.renderView(url)
                    }
                } else {
                    self.close()
                    return
                }
            }
        } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
            itemProvider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { providedText, error in
                if let error {
                    self.close()
                    return
                }
                
                guard
                    let text = providedText as? String,
                    let url = self.extractURL(text)
                else {
                    self.close()
                    return
                }
                
                DispatchQueue.main.async {
                    self.renderView(url)
                }
            }
        } else {
            self.close()
            return
        }
    }
    
    private func renderView(_ url: URL) {
        self.saveLink(url: url)
        
        // host the SwiftU view
        let contentView = UIHostingController(rootView: ShareView(url: url))
        self.addChild(contentView)
        self.view.addSubview(contentView.view)
        contentView.view.backgroundColor = .clear
        self.view.backgroundColor = .clear
        
        // set up constraints
        contentView.view.translatesAutoresizingMaskIntoConstraints = false
        contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        contentView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
        contentView.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
        
        Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in self.close() })
    }
    
    private func saveLink(url: URL) {
        // writes to a .plist file that's shared with the main app
    }
    
    private func close() {
        self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
    }
    
    private func extractURL(_ text: String) -> URL? {
        do {
            let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
            let matches = detector.matches(
                in: text,
                options: [],
                range: NSRange(location: 0, length: text.utf16.count)
            )
            
            if let firstMatch = matches.first, let range = Range(firstMatch.range, in: text) {
                let url = String(text[range])
                return URL(string: url)
            }
        } catch {
            print("Unable to extract URL from text: \(error)")
        }
        
        return nil
    }
}

扩展激活规则(Info.plist)

<key>NSExtensionActivationRule</key>
<dict>
    <key>NSExtensionActivationSupportsText</key>
    <true/>
    <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
    <integer>1</integer>
</dict>

解决方案

1. 遍历所有附件,避免仅依赖第一个

Google News可能在extensionItem.attachments中返回多个资源,第一个未必是你需要的URL/文本,修改viewDidLoad逻辑遍历所有附件:

override func viewDidLoad() {
    super.viewDidLoad()
    
    guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
        close()
        return
    }
    
    // 遍历所有附件,找到第一个符合条件的资源
    for itemProvider in extensionItem.attachments ?? [] {
        if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
            handleURLItemProvider(itemProvider)
            return
        } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
            handleTextItemProvider(itemProvider)
            return
        }
    }
    
    close()
}

// 拆分URL处理逻辑
private func handleURLItemProvider(_ provider: NSItemProvider) {
    provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] result, error in
        guard let self = self else { return }
        if let error {
            self.close()
            return
        }
        
        // 处理iOS 17+沙盒资源类型
        var targetURL: URL?
        if let sandboxedResource = result as? NSObject,
           let resourceURL = sandboxedResource.value(forKey: "resourceURL") as? URL {
            targetURL = resourceURL
        } else if let directURL = result as? URL {
            targetURL = directURL
        }
        
        if let url = targetURL {
            DispatchQueue.main.async {
                self.renderView(url)
            }
        } else {
            self.close()
        }
    }
}

// 拆分文本处理逻辑
private func handleTextItemProvider(_ provider: NSItemProvider) {
    provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { [weak self] result, error in
        guard let self = self else { return }
        if let error {
            self.close()
            return
        }
        
        guard let text = result as? String, let url = self.extractURL(text) else {
            self.close()
            return
        }
        
        DispatchQueue.main.async {
            self.renderView(url)
        }
    }
}

2. 替换Timer为视图主动触发关闭

原代码中依赖Timer延迟关闭扩展,可能在Google News的环境下Timer无法正常触发,导致卡死。修改SwiftUI视图,添加完成回调:

// ShareView.swift
struct ShareView: View {
    let url: URL
    let onComplete: () -> Void
    
    var body: some View {
        // 你的视图内容
        Button("完成") {
            onComplete()
        }
    }
}

然后在renderView中使用回调关闭扩展:

private func renderView(_ url: URL) {
    self.saveLink(url: url)
    
    let contentView = UIHostingController(rootView: ShareView(url: url, onComplete: { [weak self] in
        self?.close()
    }))
    self.addChild(contentView)
    self.view.addSubview(contentView.view)
    contentView.view.backgroundColor = .clear
    self.view.backgroundColor = .clear
    
    // 约束配置不变
    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)
}

3. 扩展激活规则,覆盖更多资源类型

修改Info.plist中的激活规则,添加更多URL相关的支持类型,确保Google News的资源能匹配到:

<key>NSExtensionActivationRule</key>
<dict>
    <key>NSExtensionActivationSupportsText</key>
    <true/>
    <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
    <integer>1</integer>
    <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
    <integer>1</integer>
    <key>NSExtensionActivationSupportsURLWithMaxCount</key>
    <integer>1</integer>
</dict>

4. 修复共享沙盒权限问题

如果saveLink方法写入共享plist时存在权限问题,会导致扩展卡死。确保主App和扩展加入同一个App Group,并且使用正确的共享目录路径:

private func saveLink(url: URL) {
    guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "你的AppGroupID") else {
        return
    }
    let plistURL = groupURL.appendingPathComponent("SharedLinks.plist")
    
    // 写入逻辑
}

调试建议
  1. 使用os.Logger替代print,确保日志能输出到设备日志:
    import os.log
    let logger = OSLog(subsystem: "你的扩展BundleID", category: "ShareExtension")
    os_log("提取URL失败: %@", log: logger, type: .error, error.localizedDescription)
    
  2. 实体设备上,通过Xcode的Window > Devices and Simulators选择设备,点击View Device Logs,搜索扩展BundleID查看崩溃日志。
  3. 给所有错误分支添加明确的close()调用,避免扩展陷入无限等待状态。

内容的提问来源于stack exchange,提问作者Scarysize

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 11:05:00