16

Lets imagine that I have an App

var storeVM = BookStoreViewModel(bla1: bla1, bla2: bla2, bla3: bla3)

@SceneBuilder var body: some Scene {
    WindowGroup {
        BookStoreView( model: storeVM )
    }
    
    #if os(macOS)
    Settings {
        SettingsView(model: config)
    }   
    #endif
}

BookStore have a Grid with a lot of books saved in some DB.

BookView can be initiated by a following way:

BookView(model: bookViewModel)

Target: to open BookView IN A NEW SEPARATED WINDOW(as example by click on the button). How can I do this?


Bonus question: How can I open SettingsView(model: config) from the code?


PS: NavigationLink is not solution for me because I not using the NavigationView.

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101

4 Answers4

6

Update: Xcode 14 / macOS 13

Now we can explicitly open needed window giving it identifier and use it in openWindow environment action.

class BookViewModel: ObservableObject {} 

class AppState: ObservableObject {
    @Published var selectedBook: BookViewModel?
}

@main
struct SwiftUI2_MacApp: App {
    @StateObject var appState = AppState()

    @SceneBuilder 
    var body: some Scene {
        WindowGroup {         // main scene
            ContentView()
              .environmentObject(appState)   // inject to update
                                             // selected book
        }

        Window("Book Viewer", id: "book") { // << here !!
            BookView(model: appState.selectedBook)
        }

        // also possible variant with injected model for group
        // WindowGroup("Book Viewer", id: "book", for: Book.self) {  book in // << here !!
        //   BookView(model: book)
        // }
    }
}

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    @Environment(\.openWindow) private var openWindow   // << !!

    var body: some View {

        // assuming selection in grid somewhere here to
        // appState.selectedBook

        Button("Open Book") {
            openWindow(value: "book")       // << here !!

        // for group variant with injected value
        // if let book = self.selectedBook {
        //    openWindow(value: "book", book)       // << here !!
        // }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • its impossible to have if( ) directly inside of WindowGroup(), need to wrap into another view; It's impossible to open init view from nullable model - I'm in any case need to return view, but returning empty view is a bad idea -I will got an empty window as result. Returning nil - will throw an error; it's looks like a hacky way to do with a following way ( I mean those injection ). But thanks for a try – Andrew_STOP_RU_WAR_IN_UA Jul 15 '20 at 14:07
  • @Andrew, is that run-time error, because it is compilable at my side? – Asperi Jul 15 '20 at 14:27
  • 3
    No, compile errors; But in any case this is not really good solution, as truly even if it will work... Really easier and cleaner code will be in case of using AppKit... Is there are really no nicer way to open brand new window in swiftUI ? – Andrew_STOP_RU_WAR_IN_UA Jul 15 '20 at 18:14
  • 2
    This compiled fine for me, but I don't get any second window popping up. (I'm using Big Sur beta, Xcode 12.2 beta) – Rob N Sep 22 '20 at 14:26
  • @RobN Facing same issue on 12.4 release + Big Sur 11.1 – Ryan Feb 15 '21 at 17:24
5

I found this answer, which worked for me in terms of being able to open a new window: https://developer.apple.com/forums/thread/651592?answerId=651132022#651132022

I'm on xcode 12.3, Swift 5.3, running Big Sur.

The following is an example of how to set things up so a button in the ContentView can be used to open the OtherView window.

@main
struct testApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        WindowGroup("OtherView") {
            OtherView()
        }
        .handlesExternalEvents(matching: Set(arrayLiteral: "*"))
    }
}

struct ContentView: View {
    @Environment(\.openURL) var openURL

    var body: some View {
       Button("Other View") {
           if let url = URL(string: "test://otherview") {
               openURL(url)
           }
       }
    }
}

struct OtherView: View {
    var body: some View {
        Text("Other View!")
    }
}

Note: Make sure to follow the URL Scheme instructions included in the linked answer (quoted here for convenience):

Now in Project->Info->URL Types type in test in the URL Schemes field (and the identifier field too) to register our app with the system.

I achieved this by editing the Info.plist file and making the additions there, i.e URL types -> URL Schemes...:

Info.plist

hillmark
  • 807
  • 8
  • 22
4

I have played with code from hillmark's answer and got some better results than his code.

Even if this code is still not the answer, this may be useful for someone.

_

Generlly this code will be useless for you in case you need to work with View based on ViewModel.

But if you need to work with View only - this is a good solution.

@main
struct TestAppApp: App {
    var body: some Scene {
        WindowGroup {
            MainView()
        }
        // magic here
        .handlesExternalEvents(matching: Set(arrayLiteral: Wnd.mainView.rawValue))
        
        WindowGroup {
            HelperView()
        }
        // magic here
        .handlesExternalEvents(matching: Set(arrayLiteral: Wnd.helperView.rawValue))
    }
}

extension TestAppApp {
    struct MainView: View {
        var body: some View {
            VStack {
                Button("Open Main View") {
                    // magic here
                    Wnd.mainView.open()
                }
                
                Button("Open Other View") {        
                    // magic here
                    Wnd.helperView.open()
                }
            }
            .padding(150)
        }
    }

    struct HelperView: View {
        var body: some View {
            HStack {
                Text("This is ") + Text("Helper View!").bold()
            }
            .padding(150)
        }
    }
}



// magic here
enum Wnd: String, CaseIterable {
    case mainView   = "MainView"
    case helperView = "OtherView"
    
    func open(){
        if let url = URL(string: "taotao://\(self.rawValue)") {
            print("opening \(self.rawValue)")
            NSWorkspace.shared.open(url)
        }
    }
}

To work with this code you need to have the following:

enter image description here

enter image description here

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
  • 1
    Did you already try to use deep links to pass the "detail information"? E.g. the book identifier as in `taotao://MainView/ShowBook/BlaBla`. Try handling the deep link using `.onOpenUrl(...)` in the MainVIew. – pd95 Mar 08 '21 at 22:21
  • No, I didn't. I'm using another system in my app than this. As this is still not enough flexible for me. – Andrew_STOP_RU_WAR_IN_UA Mar 10 '21 at 08:07
  • 1
    Detail information can be send too as long as it's a valid url. But be careful, using that link in safari will open your app. So don't trust any input you get. – user965972 Nov 06 '21 at 09:14
  • How exactly would I pass detail information to the Helper View? Like, do I have access to the full Link somehow? – LilaQ May 29 '23 at 20:54
  • Answering my own question for completion's sake: Just appending a `.onOpenURL` on the Helper View does the trick. – LilaQ May 29 '23 at 21:54
0

I recently ran into a similar problem in swiftui where I wanted to create a new desktop-level window, and I think finally found the solution.

Button("Show Bookmark") {
    // create a new NSPanel
    let window = NSPanel(contentRect: NSRect(x: 0, y: 0, width: 500, height: 500), styleMask: [.fullSizeContentView, .closable, .resizable, .titled], backing: .buffered, defer: false)
    
    // the styleMask can contails more, like borderless, hudWindow...
                    
    window.center()

    // put your swiftui view here in rootview
    window.contentView = NSHostingView(rootView: BookmarkView())
    window.makeKeyAndOrderFront(nil)

    // if you want to close the window after delay
    DispatchQueue.main.asyncAfter(deadline: .now()) {
        window.close()
    }
}
voorjaar
  • 101
  • 1
  • 4
  • is it have all of SwiftUI features? Like windows grouping/ tabs/ etc? – Andrew_STOP_RU_WAR_IN_UA May 19 '22 at 09:00
  • yeah. I think so, but I didn't try this yet, maybe you should try it yourself. My intention at that time was to create a borderless window, like the one [mentioned here](https://stackoverflow.com/a/72299845/12741063). – voorjaar May 20 '22 at 04:41
  • I believe it is not, because here is no information for SwiftUI how to group windows :) This is hack, but not native solution for SwiftUI. My solution is also hack, but it will work with SwiftUI windows features. – Andrew_STOP_RU_WAR_IN_UA May 20 '22 at 16:11
  • I'm really new at this. I'd like to remove the references to the bookstore and just display an array of String in a list, like a pop-up, in the window. Can this be done? – Nate Lockwood Feb 06 '23 at 22:06