案例场景
在macOS中使用Swift的WindowGroup创建窗口时,无法直接限制窗口大小(如最小/最大尺寸),因为因为 WindowGroup 是一个比较“自动化”的窗口容器,它由系统管理窗口生命周期,缺乏对 NSWindow 的直接控制。
例如,设置ContentView限制视图的大小。
struct ContentView: View {
var body: some View {
VStack {
Text(" \(Bundle.main.displayName)")
}
.frame(width: 500,height: 300)
}
}

但ContentView实际显示的尺寸是上次调整的尺寸。例如,上次将窗口调整为正方形,再次打开应用时,显示的窗口仍然是正方形。
无法通过WindowGroup实现默认尺寸以及限制窗口尺寸大小的行为。
因此,就需要借助AppDelegate来实现出窗口尺寸大小的限制。
AppDelegate设置窗口
1、在入口文件中配置AppDelegate:
import SwiftUI
@main
struct ImageSlimApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
// 空 Scene,窗口由 AppDelegate 管理
Settings {} // 占位,不弹出任何窗口
}
}
在入口文件中导入AppDelegate,并设置空 Scene 占位。
2、AppDelegate.swift文件:
import AppKit
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ notification: Notification) {
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.minSize = NSSize(width: 400, height: 250)
window.maxSize = NSSize(width: 600, height: 350)
window.makeKeyAndOrderFront(nil)
}
}
通过AppDelegate创建NSWindow窗口,并配置居中、关闭窗口不被释放等参数,配置minSize和maxSize尺寸,显示窗口为主窗口。
3、SwiftUI文件:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text(" \(Bundle.main.displayName)")
}
}
}
在ContentView中设置VStack视图的尺寸。
延伸问题
1、显示窗口尺寸过小的问题
在运行上面的代码时,实际显示的窗口尺寸非常小。

这是因为虽然窗口的宽度为500,高度为300:
contentRect: NSRect(x: 0, y: 0, width: 500, height: 100)
这是因为NSHostingView会自适应内容尺寸:
window.contentView = NSHostingView(rootView: ContentView())
因为ContentView没有设置frame强制布局,因此NSHostingView.fittingSize 就很小。
NSWindow就会根据它的尺寸,覆盖设置的minSize / contnetRect。
解决方案为:给ContentView设置固定大小:
struct ContentView: View {
var body: some View {
VStack {
Text(" \(Bundle.main.displayName)")
}
.frame(width: 500,height: 300)
}
}

2、尺寸未实现限制
当我们想要实现拖动窗口时,限制窗口最大、最小尺寸时,会发现在设置ContentView的frame属性后,窗口无法被拖动,同时minSize和maxSize无法实现限制。
// 正确限制尺寸
window.minSize = NSSize(width: 400, height: 250)
window.maxSize = NSSize(width: 600, height: 350)

因此,需要在ContentView中设置视图的最大和最小尺寸:
struct ContentView: View {
var body: some View {
VStack {
Text(" \(Bundle.main.displayName)")
}
.frame(minWidth: 400,minHeight: 250) // 限制最小尺寸
.frame(maxWidth: 600,maxHeight: 350) // 限制最大尺寸
}
}
注意:我这里移除了固定的frame,这是因为测试的过程中发现,如果窗口在缩小时,只能缩小到固定的frame尺寸,而不能缩小到最小尺寸,因此这里只保留了min和mac两个限制尺寸。
在AppDelegate中,也移除了window.minSize和window.maxSize两个属性,防止尺寸冲突等问题。
3、Dock栏无法弹出窗口问题
当我们使用AppDelegate手动创建窗口,而不是使用WindowGroup创建窗口时,点击Dock图标不会自动弹出窗,这是macOS应用的常规行为。

在 SwiftUI 默认使用的 WindowGroup 场景下,系统自动为管理窗口生命周期,包括:
1、应用启动自动弹出窗口。
2、从 Dock 图标点击时自动恢复上次窗口。
而当修改为使用 AppDelegate 手动创建窗口,系统就认为这是开发者自行控制窗口行为,因此不会再自动打开窗口。
解决方案:通过AppDelegate实现手动显示窗口:
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
window.makeKeyAndOrderFront(nil)
}
return true
}
这段代码是 macOS App 中 AppDelegate 的一个生命周期回调方法,用于处理用户点击 Dock 图标时的行为。
代码解析:
1、sender:当前的 NSApplication 实例。
2、hasVisibleWindows == false 表示当前没有显示的窗口。
3、makeKeyAndOrderFront(nil) 显示你保留的 NSWindow 实例。
实现过程:
当用户点击Dock中的App图标时,macOS会调用这个方法,询问应用是否重新打开窗口。
如果用户关闭窗口后,窗口被隐藏(App未退出),调用window.makeKeyAndOrderFront(nil)显示窗口。
return true表示系统可以继续处理这个点击事件。
提示:NSWindow.isReleasedWhenClosed属性为true时,在SwiftUI中关闭窗口时窗口不被销毁。
总结
通过AppDelegate可以手动创建窗口,但是并不能管理窗口的大小和限制尺寸。
需要在SwiftUI视图中设置frame的min和max属性,实现对窗口的尺寸限制。
因为手动创建并管理创建,Dock栏无法弹出窗口,就需要使用AppDelegate 的生命周期回调方法applicationShouldHandleReopen,调用window.makeKeyAndOrderFront(nil)显示窗口。
最终实现,使用SwiftUI开发macOS应用时,设置默认窗口大小以及限制窗口尺寸的功能。
相关文章
1、macOS窗口NSWindow:https://fangjunyu.com/2025/07/01/macos%e7%aa%97%e5%8f%a3nswindow/
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/07/01/macos%e5%88%a9%e7%94%a8nswindowcontroller%e5%88%9b%e5%bb%ba%e7%aa%97%e5%8f%a3%e5%ae%9e%e7%8e%b0%e6%89%93%e5%bc%80%e5%ba%94%e7%94%a8%e7%9a%84%e5%ae%9e%e9%99%85%e6%a1%88%e4%be%8b/
完整代码
1、入口文件代码:
@main
struct ImageSlimApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
// 空 Scene,窗口由 AppDelegate 管理
Settings {} // 占位,不弹出任何窗口
}
}
2、AppDelegate文件代码:
import AppKit
import SwiftUI
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
}
}
3、ContentView文件代码:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text(" \(Bundle.main.displayName)")
}
.frame(minWidth: 400,minHeight: 250) // 限制最小尺寸
.frame(maxWidth: 600,maxHeight: 350) // 限制最大尺寸
}
}
