场景分析
在macOS中创建菜单栏时,我想要实现关闭(不退出)应用窗口后,点击状态栏的菜单按钮,实现打开应用窗口的效果。

import AppKit
import SwiftUI
class StatusBarController:ObservableObject {
private var statusItem: NSStatusItem!
init() {
// 创建系统菜单栏图标
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
button.image = NSImage(named: "templateIcon")
button.toolTip = "轻压图片"
}
// 创建菜单
let menu = NSMenu()
let openItem = NSMenuItem(title: "打开 App", action: #selector(openApp), keyEquivalent: "o")
openItem.target = self
menu.addItem(openItem)
menu.addItem(NSMenuItem.separator())
let quitItem = NSMenuItem(title: "退出", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
menu.addItem(quitItem)
statusItem.menu = menu
}
@objc func openApp() {
print("打开 App")
NSApp.activate(ignoringOtherApps: true)
}
}

当关闭主窗口后,重新点击“打开App”菜单按钮时,并不会打开应用窗口。
问题分析
App启动后,点击“打开App”菜单按钮时,调用openApp方法,能够将App设置为活动应用。
当主窗口被关闭,再点击菜单按钮,调用openApp,实际上没有效果。
NSApp.activate(ignoringOtherApps: true) 确实激活了 App,但没有让主窗口重新出现。
在macOS中,当用户点击窗口左上角红叉关闭窗口时,应用仍在后台运行,但是窗口被销毁了。所以,调用NSApp.activate(…) 虽然激活了 App,但没有任何可见窗口出现。

因此,当窗口被关闭冲,如果想要通过状态栏的菜单按钮打开窗口,就需要主动创建窗口。
使用NSWindowController创建窗口
主动创建窗口:首先需要创建一个窗口控制器,通过NSApplication判断当前是否含有主窗口。
WindowManager 类:
import AppKit
import SwiftUI
class WindowManager {
static let shared = WindowManager()
var mainWindowController: NSWindowController?
func showMainWindow() {
if NSApp.windows.contains(where: {
let className = NSStringFromClass(type(of: $0))
return className.contains("AppKitWindow") || className.contains("NSWindow")
}){
print("找到 SwiftUI 主窗口")
if let window = mainWindowController {
window.showWindow(nil)
} else {
NSApp.activate(ignoringOtherApps: true)
}
} else {
print("创建新的 Window 窗口")
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.title = "主窗口"
window.center()
window.isReleasedWhenClosed = false
window.contentViewController = NSHostingController(rootView: ContentView())
let controller = NSWindowController(window: window)
controller.shouldCascadeWindows = true
controller.showWindow(nil)
self.mainWindowController = controller
}
}
}
在状态栏菜单代码中,调用WindowManager类的showMainWindow方法,从而实现创建新的窗口。
class StatusBarController:ObservableObject {
...
@objc func openApp() {
WindowManager.shared.showMainWindow() // 调用方法
}
}
当点击状态栏菜单并调用openApp方法后,会弹出自建的窗口。

代码解析
1、窗口控制器
class WindowManager {
static let shared = WindowManager()
var mainWindowController: NSWindowController?
func showMainWindow() {
在这个代码中,创建了一个WindowManager的窗口管理类,并使用static创建单例模式。
创建一个mainWindowController变量(可选的NSWindowController类型)和showMainWindow方法。
mainWindowController变量用于存储一个 NSWindowController 类型的对象引用。
showMainWindow方法则用于判断并显示主窗口。
2、判断窗口是否存在
if NSApp.windows.contains(where: {
let className = NSStringFromClass(type(of: $0))
return className.contains("AppKitWindow") || className.contains("NSWindow")
}){
print("找到 SwiftUI 主窗口")
if let window = mainWindowController {
window.showWindow(nil)
} else {
NSApp.activate(ignoringOtherApps: true)
}
} else {
print("创建新的 Window 窗口")
这是一段判断代码,从NSApp.windows获取全部的窗口。如果窗口类型中含有“AppKitWindow”或“NSWindow”,则表示当前的窗口没有关闭,可以展示出来。
这里可以在视图代码中遍历NSApp.windows了解到窗口类型:
for window in NSApp.windows {
print("window:\(window)")
}
当首次打开应用时,NSApp.windows遍历的窗口信息为:
window:<NSStatusBarWindow: 0x127a2d2b0>
window:<SwiftUI.AppKitWindow: 0x12800a000>
这表示当前应用存在两个窗口,分别为 NSStatusBarWindow(状态栏)和SwiftUI.AppKitWindow(主窗口)。

当关闭主窗口后,重新使用NSWindowController创建主窗口,NSApp.windows遍历窗口信息:
window:<NSStatusBarWindow: 0x127a2d2b0>
window:<NSWindow: 0x12792f160>
可以看到,通过NSWindowController创建的主窗口类型是NSWindow,而不是SwiftUI.AppKitWindow。
所以,就需要根据NSApp.windows显示的各窗口类型来判断,如果类型包含“AppKitWindow”或“NSWindow”,就表示现在有打开的主窗口。然后,就可以将主窗口设置为活动窗口,应用窗口显示在最前面。
if NSApp.windows.contains(where: {
let className = NSStringFromClass(type(of: $0))
return className.contains("AppKitWindow") || className.contains("NSWindow")
})
如果判断有主窗口,再判断主窗口是不是通过NSWindowController创建的窗口,因为我们通过mainWindowController这个变量存储着 NSWindowController 类型的对象引用。
print("找到 SwiftUI 主窗口")
if let window = mainWindowController {
window.showWindow(nil)
} else {
NSApp.activate(ignoringOtherApps: true)
}
如果mainWindowController变量不为nil,则表示当前的主窗口是我们通过NSWindowController创建的窗口,那么我们就调用showWindow(nil)方法,显示窗口。
如果mainWindowController变量为nil,那么就说明当前的主窗口不是我们创建的NSWindowController窗口,我们就调用NSApp.activate()方法,激活主窗口。
3、创建NSWindowController窗口
当主窗口被关闭后,想要重新显示窗口。就需要通过NSWindowController创建新的窗口。
print("创建新的 Window 窗口")
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.title = "主窗口"
window.center()
window.contentViewController = NSHostingController(rootView: ContentView())
let controller = NSWindowController(window: window)
controller.shouldCascadeWindows = true
controller.showWindow(nil)
self.mainWindowController = controller
使用NSWindow创建窗口,设置NSWindow的标题、居中,内容视图为ContentView视图。
初始化NSWindowController窗口控制器,设置级联为true,显示窗口。
将创建的NSWindowController窗口控制器的引用保存到mainWindowController变量中。
4、调用NSWindowController
最后,设置oepnApp方法,每次点击状态栏菜单项,就会调用WindowManager的showMainWindow方法。
class StatusBarController:ObservableObject {
...
@objc func openApp() {
WindowManager.shared.showMainWindow() // 显示主窗口
}
}
5、实现效果
当首次打开应用时,应用的窗口类型为SwiftUI.AppKitWindow,在状态栏中点击“打开App”,就会调用NSApp.activate()方法,将激活App。
如果把主窗口关闭,在状态栏中点击“打开App”,就会利用NSWindowController创建一个窗口。
总结
当点击Dock栏中的应用或从应用程序中打开应用时,窗口的生命周期是由 SwiftUI 的 @main App 机制管理。

当用户关闭窗口后,想要通过状态栏重新打开应用,就需要通过NSWindowController创建新的窗口。
此时窗口的类型从SwiftUI.AppKitWindow变成了NSWindow。
生命周期也从SwiftUI 的 @main App 机制变成由WindowManager类中的NSWindowController窗口控制器进行管理。
延伸问题(重复窗口问题)
虽然,现在可以实现窗口的创建,但实际上还是分成了NSWindowController创建和SwiftUI的@main App两种创建形式。
当通过NSWindowController创建窗口后,点击Dock栏的应用程序,就会创建新的窗口,这样就会存在重复打开窗口的问题。这两个窗口分别由NSWindowController和SwiftUI 的 @main App进行管理。
这个问题目前只有两种解决办法:
1、修改 App 主入口,把 WindowGroup 去掉或换成空场景:
@main
struct ImageSlimApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
// 空 Scene,窗口由 AppDelegate 管理
Settings {} // 占位,不弹出任何窗口
}
}
使用 AppDelegate 来唯一控制窗口创建。
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var statusBarController: StatusBarController?
func applicationDidFinishLaunching(_ notification: Notification) {
statusBarController = StatusBarController()
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable], // 可以调大小
backing: .buffered,
defer: false)
window.center()
window.isReleasedWhenClosed = false
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
window.makeKeyAndOrderFront(nil)
}
return true
}
}
当点击底部Dock菜单栏的应用时,会调用AooDelegate的生命周期回调方法applicationShouldHandleReopen方法,,用于处理用户点击Dock图标时的行为。
因为配置了isReleasedWhenClosed,所以在SwiftUI中不会在关闭窗口后,销毁窗口。
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool
sender:当前的 NSApplication 实例。
hasVisibleWindows(flag):当前是否有“可见”的窗口(如果所有窗口都被关闭了,就是 false)。
if !flag {
window.makeKeyAndOrderFront(nil)
}
return true
所以,如果当前没有可见窗口(用户点击关闭窗口按钮,窗口被隐藏,App未推出),就调用window.makeKeyAndOrderFront(nil)显示窗口。
return true表示系统可以继续处理这个点击事件。
2、完全使用SwiftUI + WindowGroup,放弃NSWindowController。
这也就意味着,不去通过NSWindowController创建新的窗口,不在状态栏中新增“打开”。
相关文章
1、macOS核心对象NSApplication:https://fangjunyu.com/2025/06/19/macos%e6%a0%b8%e5%bf%83%e5%af%b9%e8%b1%a1nsapplication/
2、macOS应用代理AppDelegate和NSApplicationDelegate:https://fangjunyu.com/2025/06/25/macos%e5%ba%94%e7%94%a8%e4%bb%a3%e7%90%86appdelegate/
3、macOS管理窗口的控制器类NSWindowController:https://fangjunyu.com/2025/06/30/macos%e7%ae%a1%e7%90%86%e7%aa%97%e5%8f%a3%e7%9a%84%e6%8e%a7%e5%88%b6%e5%99%a8%e7%b1%bbnswindowcontroller/