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

macOS 15 SwiftUI:如何为MenuBarExtra窗口添加Popover三角?

在macOS 15 SwiftUI中为MenuBarExtra窗口添加Popover三角标记的方案

在macOS 15的SwiftUI应用里,使用MenuBarExtra并设置.menuBarExtraStyle(.window)时,官方并未提供直接的.popover(...)修饰符,将该修饰符应用到ContentView上也无法实现带三角标记的popover窗口效果。以下是可行的替代方案:

核心思路

由于SwiftUI原生不支持为MenuBarExtra的窗口添加popover三角,我们需要借助AppKit的NSStatusItem和自定义NSWindow实现完全可控的popover样式,同时在SwiftUI视图中添加自定义三角标记。

完整实现代码

1. 自定义Popover窗口类

import Cocoa
import SwiftUI

class PopoverMenuWindow: NSWindow {
    override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) {
        super.init(contentRect: contentRect, styleMask: [.borderless, .nonactivatingPanel], backing: backingStoreType, defer: flag)
        self.isMovable = false
        self.level = .popUpMenu
        self.backgroundColor = .clear
        self.hasShadow = true
    }
    
    // 计算并设置窗口相对于状态栏按钮的位置,确保三角对齐
    func setupAnchor(at statusButton: NSStatusBarButton) {
        guard let buttonScreenFrame = statusButton.window?.convertToScreen(statusButton.frame) else { return }
        let windowX = buttonScreenFrame.midX - self.frame.width / 2
        let windowY = buttonScreenFrame.minY - self.frame.height
        self.setFrameOrigin(NSPoint(x: windowX, y: windowY))
    }
}

2. 替换原生MenuBarExtra,实现自定义popover逻辑

import SwiftUI

@main
struct MineApp: App {
    let systemImage: String = "square.fill"
    @State private var statusItem: NSStatusItem?
    @State private var popoverWindow: PopoverMenuWindow?
    @State private var isPopoverVisible = false
    
    var body: some Scene {
        // 保留Settings场景(可选)
        Settings {
            EmptyView()
        }
        
        // 隐藏原生MenuBarExtra,用自定义状态栏项替代
        MenuBarExtra("", systemImage: systemImage) {
            EmptyView()
        }
        .menuBarExtraStyle(.hidden)
        
        .onAppear {
            setupCustomStatusItem()
        }
    }
    
    private func setupCustomStatusItem() {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
        guard let button = statusItem?.button else { return }
        
        button.image = NSImage(systemSymbolName: systemImage, accessibilityDescription: nil)
        button.action = #selector(togglePopover(_:))
        button.target = self
    }
    
    @objc private func togglePopover(_ sender: Any) {
        isPopoverVisible.toggle()
        
        if isPopoverVisible {
            // 创建SwiftUI内容视图
            let contentView = ContentView()
                .frame(width: 200, height: 400)
            
            // 包装成NSHostingController
            let hostingController = NSHostingController(rootView: contentView)
            let windowRect = NSRect(x: 0, y: 0, width: 200, height: 400)
            
            // 初始化并配置自定义窗口
            popoverWindow = PopoverMenuWindow(contentRect: windowRect, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
            popoverWindow?.contentViewController = hostingController
            
            if let button = statusItem?.button {
                popoverWindow?.setupAnchor(at: button)
            }
            
            popoverWindow?.makeKeyAndOrderFront(nil)
        } else {
            popoverWindow?.close()
            popoverWindow = nil
        }
    }
}

3. 带三角标记的ContentView

struct ContentView: View {
    var body: some View {
        VStack {
            Text("菜单内容")
                .padding()
            Spacer()
        }
        .background(Color.white)
        .cornerRadius(8)
        .shadow(radius: 5)
        // 添加自定义三角标记,对齐顶部中间
        .overlay(
            Triangle()
                .fill(Color.white)
                .frame(width: 16, height: 8)
                .position(x: 100, y: -4)
                .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: -1)
            , alignment: .top
        )
    }
}

// 自定义三角形状
struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
        path.closeSubpath()
        return path
    }
}

方案说明

  • 完全通过AppKit的API实现状态栏项和弹出窗口,避免依赖SwiftUI原生MenuBarExtra的限制。
  • 自定义窗口设置为.nonactivatingPanel,确保点击外部区域时自动关闭,符合系统popover的交互逻辑。
  • 通过计算状态栏按钮的屏幕位置,精准定位窗口,让三角标记对齐按钮中心。
  • 自定义Triangle形状配合阴影,模拟系统原生popover的三角视觉效果。

内容的提问来源于stack exchange,提问作者dàhóngpāo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:10:02