macOS管理窗口的控制器类NSWindowController
macOS管理窗口的控制器类NSWindowController

macOS管理窗口的控制器类NSWindowController

NSWindowController 是 macOS 应用开发中(AppKit 框架里)用于管理窗口(NSWindow)的控制器类。

class NSWindowController : NSResponder

NSWindowController 是 macOS 上管理窗口(NSWindow)生命周期、内容视图、显示/关闭逻辑的对象,类似于 iOS 中的 UIViewController 管理一个 UIView。

基本结构

一个标准的 macOS 窗口由四个层次构成:

NSWindowController → NSWindow → NSViewController → NSView

NSWindowController:管理窗口(是否显示、关闭、设置内容控制器等)

NSWindow:窗口本体(有标题栏、边框)

NSViewController:窗口的内容控制器(页面逻辑)

NSView:实际绘制 UI 的视图

常见方法和属性

1、初始化方法

init(window: NSWindow?):用已有的 NSWindow 初始化窗口控制器。

init(windowNibName: NSNib.Name):通过 nib 文件初始化窗口(Interface Builder 中常用)。

2、显示/关闭/管理窗口

showWindow(_ sender: Any?):显示窗口,常用于用户点击后弹出窗口。

close():关闭窗口。

window:当前管理的 NSWindow 对象。

isWindowLoaded:当前窗口是否已经加载(windowDidLoad() 是否已调用)。

loadWindow():手动加载窗口(一般用于 nib 创建时)。

3、生命周期

windowDidLoad():当窗口加载完成后调用(用于初始化视图)。

windowWillLoad():在窗口即将加载前调用(较少使用)。

windowShouldClose(_:):控制窗口是否能被关闭,返回 true/false。

windowWillClose(_:):一般通过通知监听关闭事件,而不是重写这个方法。

4、设置内容控制器

contentViewController:设置或获取窗口中的主视图控制器(NSViewController)。

windowFrameAutosaveName:自动保存/恢复窗口尺寸和位置的标识名(需开启)。

5、管理窗口层级 / 其他行为

shouldCascadeWindows:是否自动级联(错开)窗口,默认 true。

owner:可选,设定拥有者对象(用得较少)。

synchronizeWindowTitleWithDocumentName:是否同步窗口标题与文档名称。

document:如果是文档窗口,可关联 NSDocument。

6、监听窗口关闭通知

NotificationCenter.default.addObserver(
    self,
    selector: #selector(windowWillClose(_:)),
    name: NSWindow.willCloseNotification,
    object: self.window
)

然后实现:

@objc func windowWillClose(_ notification: Notification) {
    // 清理资源、释放窗口控制器
}

注意:窗口关闭后,如果没有对 NSWindowController 保留强引用,它可能被释放。

示例代码

在菜单栏 app 或主应用里点击“设置”,想要打开一个专属的、可复用的设置窗口:

1、定义 SwiftUI 设置视图

struct SettingsView: View {
    var body: some View {
        VStack {
            Text("偏好设置")
            Toggle("开启某项功能", isOn: .constant(true))
        }
        .padding()
        .frame(width: 300, height: 200)
    }
}

2、创建 NSWindowController

import AppKit
import SwiftUI

class SettingsWindowController: NSWindowController {
    init() {
        let hosting = NSHostingController(rootView: SettingsView())
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 300, height: 200),
            styleMask: [.titled, .closable, . resizable],
            backing: .buffered,
            defer: false
        )
        window.contentViewController = hosting
        window.title = "设置"
        window.center()

        super.init(window: window)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

3、点击按钮弹出这个设置窗口

struct ContentView: View {
    @State private var settingsWindow: SettingsWindowController?

    var body: some View {
        Button("打开设置") {
            if settingsWindow == nil {
                settingsWindow = SettingsWindowController()
            }
            settingsWindow?.showWindow(nil)
            NSApp.activate(ignoringOtherApps: true) // 保证窗口跳到前台
        }
    }
}

注意:这里用 @State 保存 NSWindowController 实例,是为了避免窗口关闭后对象被销毁,再次点击按钮时还能复用它。

代码详解

1、创建自定义窗口控制器

class CustomWindowController: NSWindowController {

创建一个自定义的窗口控制器,继承自 NSWindowController

这是专门用来管理一个窗口及其视图内容的类。可以调用 .showWindow() 来显示这个窗口。

2、重写初始化方法

init() {

重写初始化方法,表示要以“代码方式”初始化窗口(不是通过 XIB 或 Storyboard)。

3、使用NSHostingController封装SwiftUI的视图,以便在AppKit中访问SwiftUI视图。

let hosting = NSHostingController(rootView: contentView)

4、创建NSWindow实例

let window = NSWindow(
    contentRect: NSRect(x: 100, y: 100, width: 800, height: 600),
    styleMask: [.titled, .closable, .resizable],
    backing: .buffered,
    defer: false
)

contentRect:初始窗口的位置和大小,(x: 100, y: 100, width: 800, height: 600)。

styleMask:窗口的样式,这里是“有标题栏 + 可关闭 + 可调整大小”。

backing:使用 buffered 表示双缓冲绘图。

defer:是否推迟窗口创建,通常用 false 即可。

5、NSWindow配置

window.contentViewController = hosting

设置窗口的内容控制器为NSHostingController包装过的 SwiftUI View。

window.title = "设置"

设置窗口的标题栏标题。

window.center()

把窗口居中显示(忽略 contentRect 里的 x, y)。

6、调用父类init构造函数

super.init(window: window)

调用父类的 init(window:) 构造函数,传入刚刚创建好的 NSWindow。

这一步是 NSWindowController 正确绑定窗口的关键步骤。

7、required构造函数

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

因为没有通过 XIB / Storyboard 初始化,所以不需要实现这个构造器。

通常只保留 fatalError 即可,避免 Xcode 报错。

注意:contentRect设置的NSRect并不会影响到窗口的最终尺寸。因为设置了:

window.contentViewController = NSHostingController(rootView: SettingsView())

当把一个 SwiftUI 视图封装到NSHostingController后,它内部其实是 AppKit 的 NSViewController,并且它的视图尺寸是由 SwiftUI 的 SettingsView() 布局决定的。

NSWindowController和SwiftUI区别

NSWindowController可以实现:

1、精确设置窗口初始大小:setFrame(…)。

2、控制窗口打开位置:.center(), .setFrameOrigin(…)。

3、控制窗口风格(无标题栏、透明、浮动窗口等):styleMask、level = .floating。

4、保持窗口一直在最前:window.level = .floating。

5、禁用窗口关闭按钮或拖动:standardWindowButton, isMovable。

6、自定义关闭行为(确认、拦截):windowShouldClose()。

7、多个独立窗口控制(每个窗口保活):(SwiftUI窗口关闭后失效),使用多个 NSWindowController 实例。

8、精准窗口销毁控制:windowWillCloseNotification, deinit。

9、窗口恢复上次大小位置:windowFrameAutosaveName。

SwiftUI可以实现:

1、声明式 UI 布局,NSWindowController需手写布局代码或 XIB。

2、视图状态响应(@State, @Binding)。

3、简洁 UI 组合,NSWindowController 冗长。

4、动画、导航栈、环境传递。

5、与 iOS 代码共享视图。

补充技巧

1、设置窗口尺寸、位置:window?.setFrame(…) 或初始 frame。

2、保持窗口打开时不退出 App:applicationShouldTerminateAfterLastWindowClosed 返回 false。

3、在 SwiftUI 中调用:MyWindowController().showWindow(nil)。

总结

NSWindowController 是 macOS 专用的窗口管理控制器。

它管理 NSWindow 的生命周期、显示状态、内容控制器等。

对于使用 AppKit、Storyboard/XIB 或桥接 SwiftUI 的高级用法非常重要。

相关文章

macOS显示SwiftUI的桥接控制器NSHostingController:https://fangjunyu.com/2025/06/30/macos%e6%98%be%e7%a4%baswiftui%e7%9a%84%e6%a1%a5%e6%8e%a5%e6%8e%a7%e5%88%b6%e5%99%a8nshostingcontroller/

扩展知识

NSWindowController和NSWindow的关系

NSWindowController ≠ 必须绑定 NSWindow,但它的核心功能就是为了管理 NSWindow。

通常情况下,创建 NSWindowController 的目的,就是为了控制 NSWindow 的生命周期和行为。

NSWindow是一个实际的窗口对象,NSWindowController是一个控制器,负责管理这个窗口的打开、关闭、行为逻辑。

NSWindow窗口对象可以不需要控制器,但NSWindowController控制器必须管理窗口才有意义。

NSWindow通常被NSWindowController管理,而NSWindowController通常需要持有一个window。

只用 NSWindow,不用 NSWindowController
let window = NSWindow(...)
window.makeKeyAndOrderFront(nil)

适用于:非常简单的窗口,比如:

浮动工具窗口。

调试窗口。

全屏展示图像。

弹出透明窗口。

需要手动管理其打开和关闭,不会自动释放、不会走 delegate 生命周期。

用 NSWindowController
let controller = MyWindowController()
controller.showWindow(nil)

这个时候:

MyWindowController 内部持有一个 NSWindow

可以复用控制器(防止窗口销毁)。

可以走 windowDidLoad() 生命周期。

可以响应窗口关闭、最小化等行为。

更易管理多个窗口(特别是文档、多窗口结构)。

推荐作为标准窗口管理方式。也可以绑定 XIB(传统)或手动创建窗口(现代)。

   

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

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

发表回复

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