UIViewControllerRepresentable实现的popover仅iOS14下无法更新的兼容方案求助
解决方案
问题根因
iOS 14 与 iOS 15 在 UIHostingController 尺寸计算、视图生命周期、UIPopoverPresentationController 代理生效逻辑上存在差异,原有代码主要触发了三处iOS14兼容性问题:
- iOS14 下
UIHostingController的sizeThatFits不会自动触发根视图布局,直接调用得到的内容尺寸错误,导致popover显示异常 - 基于
host.viewIfLoaded?.window的弹出状态判断在iOS14下不可靠,视图加载到挂载window存在时间差,会重复触发弹出逻辑 - 仅实现单参数的
adaptivePresentationStyle代理方法在iOS14下部分场景不生效,会导致popover被强制转为全屏模态
兼容双版本的修改后代码
struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View { @Binding var isPresented: Bool let onDismiss: (() -> Void)? let content: () -> PopoverContent func body(content: Content) -> some View { content .background( Popover( isPresented: self.$isPresented, onDismiss: self.onDismiss, content: self.content ) ) } } extension View { func cocoaPopover<Content>( isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: @escaping () -> Content ) -> some View where Content: View { ModifiedContent( content: self, modifier: PopoverViewModifier( isPresented: isPresented, onDismiss: onDismiss, content: content ) ) } } struct Popover<Content: View> : UIViewControllerRepresentable { @Binding var isPresented: Bool let onDismiss: (() -> Void)? @ViewBuilder let content: () -> Content func makeCoordinator() -> Coordinator { return Coordinator(parent: self) } func makeUIViewController(context: Context) -> UIViewController { return UIViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { let host = context.coordinator.host host.rootView = content() // 弹出逻辑 if isPresented, !context.coordinator.isPresenting { // 强制触发布局,保证iOS14下能拿到正确尺寸 host.view.layoutIfNeeded() // 计算内容自适应尺寸 let contentSize = host.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) host.preferredContentSize = contentSize host.modalPresentationStyle = .popover guard let popover = host.popoverPresentationController else { return } popover.delegate = context.coordinator // 保证sourceView布局完成,bounds正确 uiViewController.view.layoutIfNeeded() popover.sourceView = uiViewController.view popover.sourceRect = uiViewController.view.bounds context.coordinator.isPresenting = true uiViewController.present(host, animated: true) } // 收起逻辑 else if !isPresented, context.coordinator.isPresenting { context.coordinator.isPresenting = false host.dismiss(animated: true) } } class Coordinator: NSObject, UIPopoverPresentationControllerDelegate { let host: UIHostingController<Content> private let parent: Popover // 新增状态标记,避免重复弹出/收起 var isPresenting = false init(parent: Popover) { self.parent = parent // 初始化时不需要传入content,update阶段会更新rootView self.host = UIHostingController(rootView: parent.content()) } func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { parent.isPresented = false isPresenting = false parent.onDismiss?() } // 实现两个版本的代理方法,兼容iOS14 func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none } func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { return .none } } }
核心修改点说明
- 给Coordinator新增
isPresenting状态标记,替代原有基于window的判断逻辑,避免重复触发弹出/收起 - 计算内容尺寸前强制触发
UIHostingController的视图布局,使用systemLayoutSizeFitting替代sizeThatFits保证iOS14下拿到正确的内容尺寸 - 补充双参数的
adaptivePresentationStyle代理方法,保证iOS14下popover不会被强制转为全屏模态 - 获取
sourceRect前强制布局背景视图,避免弹出位置偏移 - 调整dismiss逻辑的判断条件,避免已收起的状态下重复调用dismiss方法
内容的提问来源于stack exchange,提问作者Basel
相关产品推荐
相关产品推荐

