Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI on Mac: Close Window and open another one

Tags:

swiftui

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?

like image 432
Lupurus Avatar asked Oct 21 '19 22:10

Lupurus


2 Answers

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.

like image 162
rayepps Avatar answered Sep 25 '22 15:09

rayepps


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.

like image 27
hush-entangle Avatar answered Sep 26 '22 15:09

hush-entangle