我在我的一个项目中确实遇到了同样的问题,我想我会更深入地调查一下,我发现了两种控制窗口位置的方法。
所以我影响窗口位置的第一种方法是预先定义窗口在屏幕上的最后位置。
间接控制:帧自动保存名称
当应用程序的第一个窗口打开时,macOS 将尝试恢复上次关闭时的最后一个窗口位置。为了区分不同的窗口,每个窗口都有自己的frameAutosaveName。
Windows 框架会自动以文本格式保存在应用首选项 (UserDefaults.standard) 中,密钥源自 frameAutosaveName: "NSWindow Frame "(请参阅 docs for saveFrame)。
如果您未在 WindowGroup 中指定 ID,SwiftUI 将从您的主视图类名称派生自动保存名称。前三个窗口将具有以下自动保存名称:
<ModuleName>.ContentView-1-AppWindow-1
<ModuleName>.ContentView-1-AppWindow-2
<ModuleName>.ContentView-1-AppWindow-3
通过设置一个 ID,例如 WindowGroup(id: "main"),使用以下自动保存名称(同样用于前三个窗口):
main-AppWindow-1
main-AppWindow-2
main-AppWindow-3
当您检查您的应用首选项目录(存储UserDefaults.standard)时,您将在 plist 中看到一个条目:
NSWindow Frame main-AppWindow-1 1304 545 400 228 0 0 3008 1228
有很多数字需要消化。前 4 个整数描述窗口框架(来源和大小),接下来的 4 个整数描述屏幕框架。
在手动设置这些值时需要牢记以下几点:
- macOS 坐标系的原点 (0,0) 位于左下角。
- 窗口高度包括窗口标题栏(在 macOS Monterey 上为 28px,但在其他版本上可能不同)
- 屏幕高度不包括标题栏
- 我没有关于这种格式的文档,我通过反复试验来获得有关它的知识...
所以为了伪造屏幕中心的初始位置,我使用了在应用程序(或 ContentView)初始化程序中运行的以下函数。但请记住:使用此方法只会将第一个窗口居中。以下所有窗口将被放置在前一个窗口的右侧。
func fakeWindowPositionPreferences() {
let main = NSScreen.main!
let screenWidth = main.frame.width
let screenHeightWithoutMenuBar = main.frame.height - 25 // menu bar
let visibleFrame = main.visibleFrame
let contentWidth = WIDTH
let contentHeight = HEIGHT + 28 // window title bar
let windowX = visibleFrame.midX - contentWidth/2
let windowY = visibleFrame.midY - contentHeight/2
let newFramePreference = "\(Int(windowX)) \(Int(windowY)) \(Int(contentWidth)) \(Int(contentHeight)) 0 0 \(Int(screenWidth)) \(Int(screenHeightWithoutMenuBar))"
UserDefaults.standard.set(newFramePreference, forKey: "NSWindow Frame main-AppWindow-1")
}
我的第二种方法是直接操作底层的NSWindow,类似于你的WindowAccessor。
直接控制:操作 NSWindow
您对WindowAccessor 的实现有一个特定的缺陷:您正在读取view.window 以提取NSWindow 实例的块异步运行:在未来的某个时间(由于DispatchQueue.main.async)。
这就是为什么窗口出现在屏幕上的 SwiftUI 配置位置,然后再次消失以最终移动到您想要的位置的原因。您需要更多的控制,这包括首先监视NSView 以便在设置window 属性时尽快获得通知,然后监视NSWindow 实例以了解视图何时可见。
我正在使用WindowAccessor 的以下实现。它需要一个onChange 回调闭包,只要window 发生变化就会调用它。首先它开始监视NSViews window 属性,以便在视图添加到窗口时得到通知。发生这种情况时,它开始侦听NSWindow.willCloseNotification 通知以检测窗口何时关闭。此时它将停止任何监控以避免内存泄漏。
import SwiftUI
import Combine
struct WindowAccessor: NSViewRepresentable {
let onChange: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
context.coordinator.monitorView(view)
return view
}
func updateNSView(_ view: NSView, context: Context) {
}
func makeCoordinator() -> WindowMonitor {
WindowMonitor(onChange)
}
class WindowMonitor: NSObject {
private var cancellables = Set<AnyCancellable>()
private var onChange: (NSWindow?) -> Void
init(_ onChange: @escaping (NSWindow?) -> Void) {
self.onChange = onChange
}
/// This function uses KVO to observe the `window` property of `view` and calls `onChange()`
func monitorView(_ view: NSView) {
view.publisher(for: \.window)
.removeDuplicates()
.dropFirst()
.sink { [weak self] newWindow in
guard let self = self else { return }
self.onChange(newWindow)
if let newWindow = newWindow {
self.monitorClosing(of: newWindow)
}
}
.store(in: &cancellables)
}
/// This function uses notifications to track closing of `window`
private func monitorClosing(of window: NSWindow) {
NotificationCenter.default
.publisher(for: NSWindow.willCloseNotification, object: window)
.sink { [weak self] notification in
guard let self = self else { return }
self.onChange(nil)
self.cancellables.removeAll()
}
.store(in: &cancellables)
}
}
}
然后可以使用此实现尽快获得NSWindow 的句柄。我们仍然面临的问题:我们无法完全控制窗口。我们只是在监视发生的事情并可以与NSWindow 实例进行交互。这意味着:我们可以设置位置,但我们不知道这应该在哪个时刻发生。例如。在将视图添加到窗口后直接设置窗口框架,不会产生任何影响,因为 SwiftUI 首先进行布局计算,然后再决定将窗口放置在哪里。
经过一番摆弄,我开始跟踪NSWindow.isVisible 属性。这使我可以在窗口可见时设置位置。使用上面的WindowAccessor 我的ContentView 实现如下:
import SwiftUI
import Combine
let WIDTH: CGFloat = 400
let HEIGHT: CGFloat = 200
struct ContentView: View {
@State var window : NSWindow?
@State private var cancellables = Set<AnyCancellable>()
var body: some View {
VStack {
Text("it finally works!")
.font(.largeTitle)
Text(window?.frameAutosaveName ?? "-")
}
.frame(width: WIDTH, height: HEIGHT, alignment: .center)
.background(WindowAccessor { newWindow in
if let newWindow = newWindow {
monitorVisibility(window: newWindow)
} else {
// window closed: release all references
self.window = nil
self.cancellables.removeAll()
}
})
}
private func monitorVisibility(window: NSWindow) {
window.publisher(for: \.isVisible)
.dropFirst() // we know: the first value is not interesting
.sink(receiveValue: { isVisible in
if isVisible {
self.window = window
placeWindow(window)
}
})
.store(in: &cancellables)
}
private func placeWindow(_ window: NSWindow) {
let main = NSScreen.main!
let visibleFrame = main.visibleFrame
let windowSize = window.frame.size
let windowX = visibleFrame.midX - windowSize.width/2
let windowY = visibleFrame.midY - windowSize.height/2
let desiredOrigin = CGPoint(x: windowX, y: windowY)
window.setFrameOrigin(desiredOrigin)
}
}
我希望这个解决方案可以帮助其他想要在 SwiftUI 中更好地控制窗口的人。