macOS浮动窗口NSPopover
macOS浮动窗口NSPopover

macOS浮动窗口NSPopover

NSPopover 是 macOS AppKit 提供的一个 UI 组件,用于从某个界面元素(如按钮、表格行、文本等)处弹出一个小的悬浮窗口,通常用于:显示详细信息、快速设置、小表单和子选项。

它的行为类似于 iOS/iPadOS 中的 UIPopoverPresentationController,但功能更强。

基本特性

1、锚定视图(anchoring view):弹窗从某个指定视图位置弹出。

2、可自定义内容:可以放置任何 NSViewController 或 NSView。

3、自动关闭行为:可配置点击外部是否关闭(.transient, .semitransient, .applicationDefined)。

4、动画支持:弹出时默认带有动画。

基本用法

let popover = NSPopover()
popover.contentViewController = NSHostingController(rootView: PopoverContentView())
popover.behavior = .transient  // 点击其他地方关闭
popover.contentSize = NSSize(width: 200, height: 100)

popover.show(
    relativeTo: button.bounds,
    of: button,
    preferredEdge: .maxY
)

一个 NSPopover 至少需要:

1、contentViewController:内容视图控制器,决定了弹窗内部显示什么。

2、behavior:控制用户点击外部时,Popover 是否自动关闭。

3、显示方式:通过 show(relativeTo:of:preferredEdge:) 显示在某个视图的某个边上。

常用属性

1、contentViewController(必须设置)

定义弹出窗口内部显示的内容,通常是一个 NSViewController 实例。。

popover.contentViewController = MyCustomViewController()

2、behavior(控制弹窗行为)

决定 Popover 的关闭行为。是枚举类型 NSPopover.Behavior,有三种:

1、.transient:点击外部就自动关闭。常用于临时提示。

2、.semitransient:点击同一窗口内其他元素不会关闭,点击窗口外会关闭。

3、.applicationDefined:必须手动控制关闭时机。

popover.behavior = .transient

3、contentSize(设置尺寸)

Popover 的内容视图的大小,使用 NSSize:

popover.contentSize = NSSize(width: 200, height: 150)

4、animates(是否带动画)

控制弹出或关闭时是否带动画,默认 true:

popover.animates = true

5、appearance(外观风格)

设置外观(如浅色、深色),使用 NSAppearance 或 NSAppearance.Name:

popover.appearance = NSAppearance(named: .vibrantDark)

6、isShown(是否显示)

查看 Popover 是否正在显示:

if popover.isShown {
    // 说明 popover 现在是可见的
}

7、delegate

设置代理对象,遵守 NSPopoverDelegate 协议,可响应事件,如显示前、关闭后等。

popover.delegate = self

常用方法

1、show(relativeTo:of:preferredEdge:)

这是用来显示 Popover 的关键方法。

popover.show(
    relativeTo: anchorView.bounds,     // 相对锚点区域
    of: anchorView,                    // 锚点视图
    preferredEdge: .maxY               // 弹出方向(比如在按钮下方)
)

relativeTo:矩形区域(一般是 view.bounds)。

of:目标视图。

preferredEdge:弹出方向(.minX/.maxX/.minY/.maxY)。

2、performClose(_:)

关闭 Popover:

popover.performClose(nil)

3、close()

另一种关闭方式(与 performClose 类似):

popover.close()

使用示例

在 SwiftUI + AppKit 中最常见且推荐的应用场景:状态栏浮窗(macOS 菜单栏图标 + NSPopover 弹窗)。

macOS App 在右上角系统菜单栏显示一个图标,点击图标后弹出一个浮动窗口(Popover),显示 SwiftUI 构建的界面。

1、SwiftUI 内容视图

import SwiftUI

struct PopoverContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            Text("你好,这是浮窗")
                .font(.headline)
            Divider()
            Button("退出 App") {
                NSApplication.shared.terminate(nil)
            }
        }
        .padding()
        .frame(width: 200)
    }
}

2、AppDelegate 中配置 NSPopover + NSStatusItem

在 SwiftUI 项目中使用 AppKit 时,通过 NSApplicationDelegateAdaptor 接入 AppDelegate。

import Cocoa
import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusItem: NSStatusItem!
    var popover = NSPopover()

    func applicationDidFinishLaunching(_ notification: Notification) {
        // 1. 创建状态栏图标
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)

        if let button = statusItem.button {
            button.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)
            button.action = #selector(togglePopover(_:))
            button.target = self
        }

        // 2. 配置 NSPopover
        popover.contentViewController = NSHostingController(rootView: PopoverContentView())
        popover.behavior = .transient
    }

    @objc func togglePopover() {
        if let button = statusItem.button {
            if popover.isShown {
                popover.performClose(nil)
            } else {
                popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
            }
        }
    }
}

3、SwiftUI 主入口中连接 AppDelegate

@main
struct StatusBarApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        Settings {
            EmptyView() // 没有主窗口
        }
    }
}

实现效果:

4、NSPopover代码详解

1、配置NSPopover 内容和行为

popover.contentViewController = NSHostingController(rootView: PopoverContentView())
popover.behavior = .transient

popover.contentViewController:NSPopover 的内容控制器(类型是 NSViewController?)。

NSHostingController(…):是一个 AppKit 类型,它可以“承载” SwiftUI 的 View

把用 SwiftUI 编写的视图内容包裹成一个 AppKit 的 NSViewController,并嵌入到 NSPopover 中。

2、点击图标时显示或隐藏 NSPopover

@objc func togglePopover() {
    if let button = statusItem.button {
        if popover.isShown {
            popover.performClose(nil)
        } else {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
        }
    }
}

这个方法绑定在状态栏按钮 button.action = #selector(togglePopover(_:)),当点击图标时执行。

if popover.isShown {
    popover.performClose(nil)
}

popover.isShown:是否已经显示。

如果已显示,就调用 performClose(nil) 关闭它。

如果未弹出,则显示 popover:

popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)

relativeTo: button.bounds:以按钮自身的区域作为参考框。

of: button:要锚定在哪个 NSView 上,这里是状态栏按钮。

preferredEdge: .minY:希望浮窗从按钮的下方弹出。

AppKit的坐标 Y 轴是从屏幕下往上走,所以 .minY 表示“按钮的下边缘”,就是向下弹出。

常见用途

1、状态栏弹窗:工具栏按钮点击后显示选项(如系统 Wi-Fi、音量)。

2、表格中点击某行显示详情。

3、小表单:如设置参数、快捷说明等。

总结

NSPopover 是 macOS 中用于从视图边缘弹出内容的小型面板,控制精细、行为丰富,适合用来展示轻量交互视图或信息面板。

相关文章

1、SwiftUI iPad显示浮窗popover: https://fangjunyu.com/2025/01/02/swiftui-ipad%e6%98%be%e7%a4%ba%e6%b5%ae%e7%aa%97popover/

2、macOS状态栏图标关键组件NSStatusItem:https://fangjunyu.com/2025/06/26/macos%e8%8f%9c%e5%8d%95%e6%a0%8f%e5%9b%be%e6%a0%87%e5%85%b3%e9%94%ae%e7%bb%84%e4%bb%b6nsstatusitem/

3、macOS应用代理AppDelegate:https://fangjunyu.com/2025/06/25/macos%e5%ba%94%e7%94%a8%e4%bb%a3%e7%90%86appdelegate/

扩展知识

状态栏弹窗自动关闭问题

在前面的使用示例中,behavior属性设置的 .transient 在这里实效了。点击外部,popover不会自动关闭。

这是因为状态栏按钮 (NSStatusItem.button) 没有成为主窗口(keyWindow)的 NSResponder。

NSPopover 的 transient 行为需要依赖主窗口事件分发;

由于没有激活任何窗口,所以 NSPopover 没有正确地监控外部点击事件;

所以它“看不到”外部点击,也就不会关闭。

解决方法

可以手动监听全局点击事件(NSEvent.addGlobalMonitorForEvents),当检测到鼠标点击事件,就关闭 popover。

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusItem: NSStatusItem!
    var popover = NSPopover()
    var eventMonitor: Any?

    func applicationDidFinishLaunching(_ notification: Notification) {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)

        if let button = statusItem.button {
            button.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)
            button.action = #selector(togglePopover(_:))
            button.target = self
        }

        popover.contentViewController = NSHostingController(rootView: PopoverContentView())
        popover.behavior = .transient

        // 添加点击外部时关闭的监听器
        eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in
            if let strongSelf = self, strongSelf.popover.isShown {
                strongSelf.popover.performClose(nil)
            }
        }
    }

    @objc func togglePopover() {
        if let button = statusItem.button {
            if popover.isShown {
                popover.performClose(nil)
            } else {
                popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
                NSApp.activate(ignoringOtherApps: true) // 激活 App,提高事件响应
            }
        }
    }

    func applicationWillTerminate(_ notification: Notification) {
        if let monitor = eventMonitor {
            NSEvent.removeMonitor(monitor)
        }
    }
}
   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注