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

iOS17弹出键盘后SwiftUI导航/工具栏按钮失效问题求助

iOS17下QLPreviewController封装后导航栏按钮键盘弹出后失效的解决方案

问题背景

在SwiftUI中封装QLPreviewController到UIViewControllerRepresentable并包裹在UINavigationController中时,iOS17会出现键盘弹出后顶部导航栏/工具栏按钮完全无响应的问题,iOS16下功能正常。移除外层UINavigationController后问题消失,但会丢失PDF的绘图、编辑等核心控件,同时控制台会输出警告Could not update done button menu because it was not found #PreviewController。

解决方案尝试

方案1:移除外层UINavigationController,改用SwiftUI NavigationStack承载工具栏

这是最贴合SwiftUI设计的方案,避免多层导航控制器的层级冲突:

  1. 修改QLPDFPreviewController的实现,直接返回QLPreviewController而非包裹的导航控制器:
struct QLPDFPreviewController: UIViewControllerRepresentable {
    let url: URL?
    var dismissAction: (() -> Void)? // 新增dismiss回调
    
    func makeUIViewController(context: Context) -> QLPreviewController {
        let controller = QLPreviewController()
        controller.dataSource = context.coordinator
        controller.delegate = context.coordinator
        controller.allowsEditing = true // 确保保留编辑/绘图控件
        return controller
    }
    
    func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
        let parent: QLPDFPreviewController
        
        init(parent: QLPDFPreviewController) {
            self.parent = parent
        }
        
        // 原有数据源/代理代码不变
        func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
            return 1
        }
        
        func previewController(_ controller: QLPreviewController,
                               previewItemAt index: Int) -> QLPreviewItem {
            guard let url = parent.url else { return NSURL(string: "")! }
            return url as NSURL
        }
        
        func previewController(_ controller: QLPreviewController,
                               editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
            .updateContents
        }
        
        func previewController(_ controller: QLPreviewController,
                               didUpdateContentsOf previewItem: QLPreviewItem) {
            print("did some updates")
        }
        
        func previewController(_ controller: QLPreviewController,
                               didSaveEditedCopyOf previewItem: QLPreviewItem,
                               at modifiedContentsURL: URL) {
            guard let url = parent.url else { return }
            try? FileManager.default.moveItem(at: modifiedContentsURL, to: url)
            print("moved")
        }
        
        // 处理QLPreviewController的默认关闭事件
        func previewControllerDidDismiss(_ controller: QLPreviewController) {
            parent.dismissAction?()
        }
    }
}
  1. 调整QLPDFView的布局,统一用SwiftUI的NavigationStack承载工具栏,不再根据formMode分支:
struct QLPDFView: View, KeyboardPublishing {
    @Environment(\.dismiss) var dismiss
    @StateObject var pdfViewerViewModel: PDFViewerViewModel
    @State private var shouldShowCancelAlert = false
    
    var body: some View {
        NavigationStack {
            content
        }
    }
    
    private var content: some View {
        ZStack {
            if pdfViewerViewModel.isLoading {
                ProgressView()
            } else {
                QLPDFPreviewController(url: pdfViewerViewModel.pdfFileURL, dismissAction: {
                    dismiss()
                })
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Save", action: dismiss)
                            .bold()
                    }
                    
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("Cancel") {
                            shouldShowCancelAlert = true
                        }
                        .alert(isPresented: $shouldShowCancelAlert) {
                            Alert(title: Text("Warning"),
                                  message: Text("Do you want to overwrite?"),
                                  primaryButton: .cancel(Text("Stay")),
                                  secondaryButton: .destructive(Text("Leave")) {
                                pdfViewerViewModel.shouldSaveData = false
                                dismiss()
                            })
                        }
                    }
                }
                .navigationBarBackButtonHidden(true)
            }
        }
        .onAppear {
            pdfViewerViewModel.configure()
        }
    }
}

方案2:修复多层UINavigationController的交互冲突(适用于必须保留外层导航的场景)

如果业务需要保留外层的UINavigationController,可以通过调整视图层级和交互属性修复:

  1. 修改QLPDFPreviewController的makeUIViewController方法,处理导航栏冲突:
func makeUIViewController(context: Context) -> UINavigationController {
    let controller = QLPreviewController()
    controller.dataSource = context.coordinator
    controller.delegate = context.coordinator
    
    // 隐藏QLPreviewController自身的导航栏,避免双层导航冲突
    controller.navigationItem.setHidesBackButton(true, animated: false)
    
    let navigationController = UINavigationController(rootViewController: controller)
    
    // iOS17下强制确保导航栏交互可用
    navigationController.navigationBar.isUserInteractionEnabled = true
    navigationController.navigationBar.translatesAutoresizingMaskIntoConstraints = false
    
    // 监听键盘通知,在键盘弹出/收起时重置导航栏状态
    NotificationCenter.default.addObserver(
        context.coordinator,
        selector: #selector(context.coordinator.resetNavigationBarInteraction),
        name: UIResponder.keyboardDidShowNotification,
        object: nil
    )
    NotificationCenter.default.addObserver(
        context.coordinator,
        selector: #selector(context.coordinator.resetNavigationBarInteraction),
        name: UIResponder.keyboardDidHideNotification,
        object: nil
    )
    
    return navigationController
}
  1. 在Coordinator中添加重置导航栏的方法:
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
    let parent: QLPDFPreviewController
    
    init(parent: QLPDFPreviewController) {
        self.parent = parent
    }
    
    // 原有数据源/代理代码不变...
    
    @objc func resetNavigationBarInteraction() {
        DispatchQueue.main.async {
            if let navVC = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
                navVC.navigationBar.isUserInteractionEnabled = true
            }
        }
    }
}

说明

  • 方案1优先推荐,因为它遵循SwiftUI的导航范式,从根源上避免了UIKit和SwiftUI导航组件的冲突,同时完整保留QLPreviewController的编辑功能。
  • 方案2属于临时修复,针对无法重构导航结构的场景,通过监听键盘事件强制恢复导航栏交互。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 03:13:11