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设计的方案,避免多层导航控制器的层级冲突:
- 修改
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?() } } }
- 调整
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,可以通过调整视图层级和交互属性修复:
- 修改
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 }
- 在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
相关产品推荐
相关产品推荐

