Well it seems we’re not out of the woods just yet. Because when I use this code….
Swift:
@main
struct SwiftUITestingApp: {
var body: some Scene {
WindowGroup {
ContentView().frame(minWidth: 1920, idealWidth: 2560, maxWidth: 3840, minHeight: 1080, idealHeight: 1440, maxHeight: 2880, alignment: center)
}
}
}
I get the error “Cannot find ‘center’ in scope” (I created this problem by not typing your code correctly).
So I inserted a dot like this…. alignment: .center
The error goes away and I start the app. The app starts in the center of the screen. Then I move the window horizontally half way off the screen. Then I stop the app and then I run the app. And then sometimes the app doesn’t start in the center of the screen/it’s in the ‘half way off the screen’ position.
I tried this (because in Help > Developer Documentation > Search > Center > Swiftui it says)…..
center: VerticalAlignment)
I get an error… “Extra argument 'center' in call”
I also tried center() but that also didn’t fix it.
So, any thoughts about how to fix this?
OK, so I'm going to break this down a bit. First and foremost though, the dot notation was the correct one and all the behaviour you saw with it was expected and correct. I will show you a way of achieving a fresh entering every time later, but I'm going to do pull back a few steps first. John Mayer once said "My advice is that when you learn something, go back and learn the things that form the foundation for the thing you just learned.
I think it's a good idea to understand a bit more about Swift before you dig deeper with SwiftUI.
Let's look into what .center actually means and why it works.
So the method signature for the frame modifier looks like this
As you can see here, the "alignment" parameter takes as input an Alignment. An Alignment can be initiated with separate vertical and horizontal alignments, but has static class variables we can use that define common overall alignments.
When we here just say
.center
what we're actually saying is
Alignment.center
We are accessing the pre-made Alignment object called center. The reason we can omit the Alignment in the beginning is that it is implicitly inferred because the function takes an Alignment. You can try this out for yourself.
Swift:
enum Option {
case A, B, C
}
func doSomething(what: Option) {
print(what)
}
doSomething(what: .A)
As you can see, we can call the doSomething function and pass it simply .A, with the Option. being implied.
Now let's talk about the behaviour you're seeing and why you're seeing it.
The center alignment on the frame here does, perhaps confusingly, not refer to the placement of the window itself. It refers to an invisible frame around the content that the window then holds. Changing the alignment will alter the positioning of items within the window, not the placement of the window itself.
Furthermore, macOS, as intended behaviour, stores the location of windows when an application is quit so they can be placed the same location again on reopening for the user. If you close the window and not the app itself it will not be saved.
In AppKit this requires a little bit of work on the developer's side, but SwiftUI has it as default behaviour. In fact, the way SwiftUI is intended to be used you can't really even get the NSWindow object itself without it feeling like a bit of a hack. - SwiftUI is not entirely as feature rich as AppKit yet, so a lot of developers will use SwiftUI where they can but fallback to AppKit/UIKit as necessary.
The following is a pretty good article that describes some ways of working around SwiftUI limitations in the WindowGroup system, but also explains benefits of it
Issue #789 Using WindowGroup New in SwiftUI 2.0 for iOS 14 and macOS 11.0 is WindwGroup which is used in App protocol. WindowGroup is ideal for document based applications where you can open multiple windows for different content or files. For example if you’re developing a text editor or...
onmyway133.com
Now. There are two ways I have for you to tackling the particular problem at hand. One of them is more elegant from a SwiftUI point of view (though not entirely), but gives a slightly worse user experience Imo and forces ALL windows to start in the center of the screen and upon an application restart it will only snap the key window (currently active) back to the center.
Now that last point of course isn't a problem if you also intend to limit this to one window (see the linked article - App groups support multiple windows by default)
This is the idea
Swift:
//
// SwiftUITestingApp.swift
// SwiftUITesting
//
// Created by Casper Sørensen on 15/07/2021.
//
import SwiftUI
import AppKit
@main
struct SwiftUITestingApp: App {
@State private var window: NSWindow?
var body: some Scene {
window?.center()
return WindowGroup {
ContentView().frame(minWidth: 1920, idealWidth: 2560, maxWidth: 3840, minHeight: 1080, idealHeight: 1440, maxHeight: 2880, alignment: Alignment.center)
.background(WindowAccessor(window: $window))
}
}
}
struct WindowAccessor: NSViewRepresentable {
@Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
Not a lot changes in the App struct, but we need to have a lot of fluff there in the ViewRepresentable to get the window object to center. Also noteworthy is that this code is not cross platform.
The other solution is in my opinion better but the downside is that we'll have to leave the SwiftUI Lifecycle and use the AppKit lifecycle instead. I personally prefer the AppKit lifecycle for now though since SwiftUI doesn't always allow you to tweak everything, so the old AppKit patterns can come in handy and you can always wrap things up to use SwiftUI where appropriate. It looks like this in the AppDelegate:
Swift:
import Cocoa
import SwiftUI
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1920, height: 1080),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
In fact this is **almost** Xcode's default AppKit lifecycle SwiftUI project. All I've changed is that I've removed the frameAutoSave line as well as changed the size of the NSWindow that gets created to host the SwiftUI View.
If you want to take this approach I recommend just creating a new Xcode project with the AppKit lifecycle and dumping your SwiftUI files back into that from your current project, as it's easier than setting up a storyboard and AppDelegate yourself.
This way of doing it doesn't have the "snapping" behaviour visible in the prior SwiftUI lifecycle code snippet where you can briefly see the window in its former location before it moves back. It is more AppKit mindset than SwiftUI mindset however, but I couldn't personally find a pure SwiftUI solution to preventing window location autosave and restore. I feel like it ought to work to remove the UserDefaults' key for the window but it seems SwiftUI stores it differently or something. Keep in mind SwiftUI is still a fairly new framework so may not on its own offer all desired functionality. It's primary purpose is making the most common apps easier to make. I guess preventing the system from saving window state hasn't been in the cards for that, so we use some older AppKit behaviour to get it
- At least that's my solution for it. Others may very well exist - most likely do - but I looked around and couldn't find anything pure and clean SwiftUI