Reputation: 327
By default, on a macOS app using SwiftUI the window size is not restored after the window is closed.
Is there a way to keep whatever size & position the user gave before closing the app. Essentially I'd like close & open to behave in the same way to when the user quits & opens the app?
Is there something that should be added here?
import SwiftUI
@main
struct testApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Upvotes: 8
Views: 1591
Reputation: 41
I ended up solving it in this way:
I made my AppDelegate conform to NSWindowDelegate so I could have access to some of the window lifecycle methods.
I saved the window position and size to UserDefaults whenever windowWillClose
triggers.
Originally, I was trying to use windowDidBecomeVisible
as the proxy for when the window was being open again (after being closed), but it wasn't triggering.
Instead, I had to use windowDidBecomeKey
. So every time windowDidBecomeKey runs, I grab the sizes and positions from UserDefaults and use window.setFrame to set the position.
Because windowDidBecomeKey also runs whenever the window gets unfocused and refocused (e.g. during cmd-tab), I had to create a flag for windowWasClosed
to only trigger my size updates when windowDidBecomeKey represents a new window opening and not just it going back into focus.
For me, this led to the window size changing to be instant for the user. Because windowDidBecomeKey
also runs on first launch, it will also restore sizing and position from a cold start.
Here's the whole code snippet in case others in the future find it useful:
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var window: NSWindow!
var windowWasClosed = false
func applicationWillFinishLaunching(_ notification: Notification) {
NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey(_:)), name: NSWindow.didBecomeKeyNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(_:)), name: NSWindow.willCloseNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(windowDidResizeOrMove(_:)), name: NSWindow.didMoveNotification, object: window)
NotificationCenter.default.addObserver(self, selector: #selector(windowDidResizeOrMove(_:)), name: NSWindow.didResizeNotification, object: window)
}
@objc func windowDidBecomeKey(_ notification: Notification) {
if let window = notification.object as? NSWindow {
if windowWasClosed {
let windowOriginX = UserDefaults.standard.double(forKey: "windowOriginX")
let windowOriginY = UserDefaults.standard.double(forKey: "windowOriginY")
let windowWidth = UserDefaults.standard.double(forKey: "windowWidth")
let windowHeight = UserDefaults.standard.double(forKey: "windowHeight")
var frame = window.frame
frame.origin.x = windowOriginX
frame.origin.y = windowOriginY
frame.size.width = windowWidth
frame.size.height = windowHeight
window.setFrame(frame, display: true)
windowWasClosed = false
}
}
}
@objc func windowDidResizeOrMove(_ notification: Notification) {
if let window = notification.object as? NSWindow {
saveWindowPositionAndSize(window)
}
}
@objc func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow {
windowWasClosed = true
saveWindowPositionAndSize(window)
}
}
func saveWindowPositionAndSize(_ window: NSWindow) {
let windowFrame = window.frame
UserDefaults.standard.set(windowFrame.origin.x, forKey: "windowOriginX")
UserDefaults.standard.set(windowFrame.origin.y, forKey: "windowOriginY")
UserDefaults.standard.set(windowFrame.size.width, forKey: "windowWidth")
UserDefaults.standard.set(windowFrame.size.height, forKey: "windowHeight")
}
}
Upvotes: 0
Reputation: 6817
For me, neither of the suggested approaches worked to restore the window frame over app restarts. SwiftUI would always reset it.
So, I ended up manually saving and restoring the window frame from the user defaults:
func applicationDidFinishLaunching(_ notification: Notification) {
// Set window delegate so we get close notifications
NSApp.windows.first?.delegate = self
// Restore last window frame
if let frameDescription = UserDefaults.standard.string(forKey: "MainWindowFrame") {
// To prevent the window from jumping we hide it
mainWindow.orderOut(nil)
Task { @MainActor in
// Setting the frame only works after a short delay
try? await Task.sleep(for: .seconds(0.5))
mainWindow.setFrame(from: frameDescription)
// Show the window
mainWindow.makeKeyAndOrderFront(nil)
}
}
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
if let mainWindow = NSApp.windows.first {
UserDefaults.standard.set(mainWindow.frameDescriptor, forKey: "MainWindowFrame")
}
return true
}
func applicationWillTerminate(_ notification: Notification) {
if let mainWindow = NSApp.windows.first {
UserDefaults.standard.set(mainWindow.frameDescriptor, forKey: "MainWindowFrame")
}
}
Upvotes: 2
Reputation: 11
Well, I've tried the Mark G solution and it worked, but my App menu hides from macOS top Menu Bar.
So, I've reached out to this solution:
@main
struct TestingApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
let mainWindow = NSApp.windows.first
mainWindow?.delegate = self
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
let mainWindow = NSApp.windows.first
if flag {
mainWindow?.orderFront(nil)
} else {
mainWindow?.makeKeyAndOrderFront(nil)
}
return true
}
}
In this case, we need to set the main Window delegate to NSWindowDelegate
and the default implementation for windowShouldClose
is true
.
When you close the app and select the App icon from dock, it doesn't open.
So you need to implement applicationShouldHandleReopen
method.
Here's a solution demo where you can see the app is restored with the same position and size:
Link to the Demo using the default Xcode project Hello World
Upvotes: 1
Reputation: 184
I find out a way to work around this case by instead of closing this window we will show/hide it.
Here is how I did in my app
@main
struct PulltodoApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
}
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
let mainWindow = NSApp.windows[0]
mainWindow?.delegate = self
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
NSApp.hide(nil)
return false
}
}
Upvotes: 6