Reputation: 4189
I'm new to SwiftUI and it's so much to overthink after using storboards for years. I try to convert my previous storyboard application and unfortunately, there are only tutorials for iOS. Nevermind, my question...:
My application will start with a login window. If the login is successful, the window should close and a new window with the main application should appear. Now I'm sitting in front of my login window and an empty button function:
AppDelegate.swift:
let contentView = LoginView()
...
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
LoginWindow.swift:
Button(action: {
}) {
Text("Login")
}
What should I write into the button action? How and where do I call the login function and how will this function change the windows, if the login is successful?
Upvotes: 15
Views: 10878
Reputation: 536
For macOS 13
The safer approach, I think is to rely on the window id, and not on the fact that the window you are trying to close is the key
window:
Window("My title", id: "my-window-id") { ... }
To close it:
NSApplication.shared.windows.first { window in
window.identifier?.rawValue == "my-window-id"
}?.close()
Upvotes: 0
Reputation: 11
from Apple SwiftUI documents:
@main
struct Notes: App {
var body: some Scene {
WindowGroup(for: Note.ID.self) { $noteID in
// ...
}
}
}
struct NewNoteWindow: View {
var note: Note
@Environment(\.openWindow) private var openWindow
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Open Note In New Window") {
openWindow(value: note.id)
{ or dismiss() }
}
}
}
Upvotes: 0
Reputation: 8303
From macOS 13+ can use new APIs:
@main
struct Mail: App {
var body: some Scene {
WindowGroup(id: "mail-viewer") {
MailViewer()
}
}
}
struct NewViewerButton: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("Open new mail viewer") {
openWindow(id: "mail-viewer")
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
Window("Auxiliary", id: "auxiliary") {
AuxiliaryContentView()
}
}
}
struct DismissWindowButton: View {
@Environment(\.dismissWindow) private var dismissWindow
var body: some View {
Button("Close Auxiliary Window") {
dismissWindow(id: "auxiliary")
}
}
}
Upvotes: 2
Reputation: 521
I put together a quick little Swift playground demonstrating how I solved this problem. It involves a helper function which creates the variable for the window, does some setup, then sets the content to a SwiftUI view and passes the variable to that SwiftUI view. This gives the view a reference to the window which contains it, letting it call close() on that window.
import SwiftUI
func showWindow() {
var windowRef:NSWindow
windowRef = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView],
backing: .buffered, defer: false)
windowRef.contentView = NSHostingView(rootView: MyView(myWindow: windowRef))
windowRef.makeKeyAndOrderFront(nil)
}
struct MyView: View {
let myWindow:NSWindow?
var body: some View {
VStack{
Text("This is in a separate window.")
HStack{
Button(action:{
showWindow()
}) {
Text("Open another window")
}
Button(action:{
self.myWindow!.close()
}) {
Text("Close this window")
}
}
}
.padding()
}
}
showWindow()
I make the myWindow variable an optional so you can pass nil in Xcode's previews, like so:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(myWindow: nil)
}
}
Edited to add: I realized I didn't directly answer the questions posed by the OP: What should I write into the button action? How and where do I call the login function and how will this function change the windows, if the login is successful?
I have an application with a similar pattern (login, then show a different window with data from the server), which is why I had to figure out the code for the playground above. I built a class to represent the connection to the service I am using. That object's initializer can throw if it encounters an error, leaving you with a thrown error and no object.
For the button action, I use code like this (I haven't actually run this exact code, so there may be errors):
Button(action: {
let loginResult = Result {try connectToServer(self.address, username: self.username, password: self.password)}
switch loginResult {
case .failure(let error):
print(error.localizedDescription)
let loginAlert = NSAlert(error: error)
loginAlert.beginSheetModal(for: self.myWindow, completionHandler: {...})
case .success(let serverConnection):
showContentWindow(serverConnection)
self.myWindow.close()
}
}) {
Text("Login")
}
showContentWindow is a helper function like the one in the playground above. It accepts the object representing the API connection, then passes that to the SwiftUI view it uses as the contents of the window just like the one above passes the window to the view inside the window.
You can obviously handle errors in a lot of ways. The code above is close to what I use (though I don't have it sitting in front of me right now), and gives me free localized descriptions of errors like network timeouts. Error handling specifics are well beyond the scope of this, though.
The important thing is on success, it calls the function to open the new window, then calls close on its own window. Nice and simple.
Upvotes: 15
Reputation: 1164
An option might be to add a Notification observer to your AppDelegate, maintain a ref to the second window in your app delegate, and when the message to open or close the window is received, have the app delegate do that work. You can refuse to open the second window (again) if your ref isn't null thus ensuring you can't open any duplicates of your second window, or if that's not a worry for you, you can shove all your refs into a collection with some sort of unique value as its key/id, open and close as many as you want, using the window's key/id in the Notification object's userinfo.
SwiftUI is very cool, but like all works in progress, it needs some market feedback and some polish to get there. I'm not sure I'm at all in love with its current-state window handling utility in a multi-window application on MacOS. Works fine on iOS, and clearly that's its first and best use case. But it's easy to delegate that down (or laterally) to real Swift code for a more structured approach like we used to do back in the old days of like two years ago :)
Upvotes: 0
Reputation: 2092
Here is what I found works.
Close the current window
You can get the current window from the shared application instance and call close on it.
NSApplication.shared.keyWindow?.close()
Open a new window with your application view
I found that you can create an NSWindow
instance anywhere and it will get added to the screen.
let window = NSWindow(contentRect: NSRect(x: 20, y: 20, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: ResourceListView(resources: []))
window.makeKeyAndOrderFront(nil)
In the button action
Button(action: {
NSApplication.shared.keyWindow?.close()
let window = NSWindow(contentRect: NSRect(x: 20, y: 20, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: MainViewForYourNewWindow())
window.makeKeyAndOrderFront(nil)
}) {
Text("Login")
}
NOTE: I'm almost positive this is bad (not the correct way to do things). At the very least it could use some organization/design. Possibly a router of sorts that manages this open/close/route logic in one place.
Upvotes: 7