Restoring macOS window size after close using SwiftUI WindowsGroup
Asked Answered
H

4

8

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?

enter image description here

Is there something that should be added here?

import SwiftUI

@main
struct testApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Hale answered 17/3, 2022 at 2:30 Comment(6)
Unfortunately this is one of those things that SwiftUI lacks to cover until now, I believe you can solve the issue with app kit, but that would not be 100% okay, because appkit would manipulate view after view get appeared through a notification of an active window, then you would see view would appear in wrong position or size for a some moment then appkit would correct it, in general not a satisfying experience for you or your app users.Abdominal
Works fine with Xcode 13.2 / macOS 12.2. Would you show your ContentView? Or provide minimal reproducible example?Worshipful
If you create a new project in Xcode, and for ContentView() you use a TextEditor (i.e. something with a dynamic size) this issue will show. Apparently in cocoa you can use autoSaveName to get around it but not here.Hale
@Hale - did you find a solution to this problem, other than Mark's hide window solution below?Eeg
Not really, I'm using what Mark suggestedHale
Did you check out https://mcmap.net/q/1168384/-how-set-position-of-window-on-the-desktop-in-swiftui already?Singlehandedly
F
6

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
    }
}

enter image description here

Fluting answered 9/6, 2022 at 10:2 Comment(2)
Even though this is the accepted solution, I personally cannot recommend this workaround from user experience point of view. This workaround will also not restore the window state on cold startSinglehandedly
Please also keep in mind that the app submission will get rejected because there’s no menu shown for the app after the main window is closed.Talent
T
2

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")
    }
}
Torritorricelli answered 29/7, 2023 at 9:34 Comment(2)
Good, approach. What is not working out for is mainWindow.orderOut(nil). For some reason, this is closing my menubar app...Singlehandedly
I can also recommend reading https://mcmap.net/q/1168384/-how-set-position-of-window-on-the-desktop-in-swiftuiSinglehandedly
H
1

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

Highball answered 16/5, 2023 at 4:30 Comment(0)
C
0

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")
   }
}
Clarsach answered 31/8, 2024 at 6:43 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.