MMV
MMV

Reputation: 327

Restoring macOS window size after close using SwiftUI WindowsGroup

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

Upvotes: 8

Views: 1591

Answers (4)

Johnson Fung
Johnson Fung

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

codingFriend1
codingFriend1

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

kioflip
kioflip

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

Mark G
Mark G

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

enter image description here

Upvotes: 6

Related Questions