如何让.principal位置的popover与.primaryAction位置表现一致?
iPadOS 26中Toolbar不同位置Popover表现一致的实现方案
问题现象
在iPadOS 26中,Toolbar内不同位置的按钮绑定Popover时表现存在差异:
- 置于
.primaryAction位置的按钮,点击后会消失并“变形”为Popover;若使用ToolbarItemGroup,整个组都会触发该变形效果 - 置于
.principal位置的按钮(或组)无上述变形效果,仅弹出普通Popover - 但
Menu()组件在两个位置的表现完全一致
原代码示例
import SwiftUI struct PopoversView: View { @State private var showingPopover = false @State private var showingPrimaryPopover = false var body: some View { NavigationStack { Text("Hello, World!") .toolbar { ToolbarItem(placement: .principal) { Button("", systemImage: "text.bubble") { showingPopover.toggle() } //Add liquid glass style: //.padding(8).buttonStyle(.glassProminent).glassEffect(.regular.interactive()) .popover(isPresented: $showingPopover, arrowEdge: .top) { Text("This is a Popover, hi!") } } ToolbarItem(placement: .primaryAction) { Button("", systemImage: "text.bubble") { showingPrimaryPopover.toggle() } .popover(isPresented: $showingPrimaryPopover, arrowEdge: .top) { Text("This is a Primary Popover, hello!") } } } } } }
演示效果

解决方案
方案一:手动模拟系统变形动画
通过控制按钮的显示/隐藏状态,配合过渡动画,模拟.primaryAction位置的原生变形效果:
import SwiftUI struct PopoversView: View { @State private var showingPopover = false @State private var showingPrimaryPopover = false var body: some View { NavigationStack { Text("Hello, World!") .toolbar { ToolbarItem(placement: .principal) { ZStack { // 原按钮:根据Popover状态切换显示,添加过渡动画 Button("", systemImage: "text.bubble") { showingPopover.toggle() } .opacity(showingPopover ? 0 : 1) .animation(.easeInOut(duration: 0.2), value: showingPopover) // 占位视图:保持Toolbar布局稳定,避免按钮消失后布局偏移 Color.clear .frame(width: 32, height: 32) } .popover(isPresented: $showingPopover, arrowEdge: .top) { Text("This is a Popover, hi!") .onDisappear { showingPopover = false } } } ToolbarItem(placement: .primaryAction) { Button("", systemImage: "text.bubble") { showingPrimaryPopover.toggle() } .popover(isPresented: $showingPrimaryPopover, arrowEdge: .top) { Text("This is a Primary Popover, hello!") } } } } } }
方案二:封装UIKit原生Popover
利用UIKit的UIPopoverPresentationController实现更贴近原生的效果,通过UIViewControllerRepresentable封装后在SwiftUI中使用:
import SwiftUI import UIKit struct UIKitPopover<Content: View>: UIViewControllerRepresentable { @Binding var isPresented: Bool let arrowEdge: UIRectEdge let content: () -> Content func makeUIViewController(context: Context) -> UIHostingController<Content> { let host = UIHostingController(rootView: content()) host.modalPresentationStyle = .popover return host } func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) { uiViewController.rootView = content() if isPresented { uiViewController.popoverPresentationController?.permittedArrowDirections = arrowEdge uiViewController.popoverPresentationController?.sourceView = context.coordinator.sourceView uiViewController.popoverPresentationController?.sourceRect = context.coordinator.sourceRect uiViewController.presentingViewController?.present(uiViewController, animated: true) } else { uiViewController.dismiss(animated: true) } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject { let parent: UIKitPopover var sourceView: UIView? var sourceRect: CGRect = .zero init(parent: UIKitPopover) { self.parent = parent } } } extension View { func uikitPopover<Content: View>(isPresented: Binding<Bool>, arrowEdge: UIRectEdge, content: @escaping () -> Content) -> some View { self.background( UIKitPopover(isPresented: isPresented, arrowEdge: arrowEdge, content: content) .onPreferenceChange(SourceRectPreferenceKey.self) { value in guard let coordinator = self.findCoordinator() as? UIKitPopover<Content>.Coordinator else { return } coordinator.sourceView = value?.0 coordinator.sourceRect = value?.1 ?? .zero } ) } private func findCoordinator() -> Any? { // 可根据实际项目完善Coordinator查找逻辑 return nil } } struct SourceRectPreferenceKey: PreferenceKey { static var defaultValue: (UIView?, CGRect)? = nil static func reduce(value: inout (UIView?, CGRect)?, nextValue: () -> (UIView?, CGRect)?) { value = nextValue() } }
使用时只需将.principal位置的.popover替换为.uikitPopover,即可实现与.primaryAction一致的原生变形效果。
内容的提问来源于stack exchange,提问作者Anna
相关产品推荐
相关产品推荐

