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

如何让.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!")
                            }
                    }
                }
        }
    }
}

演示效果

Toolbar on iPadOS 26

解决方案

方案一:手动模拟系统变形动画

通过控制按钮的显示/隐藏状态,配合过渡动画,模拟.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 04:43:11