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)
}
}
}