Search code examples
macosswiftuiobservablemenuitem

Passing a Value from App to ContentView in SwiftUI


I find a lot of resources out there as to how to let the user select a menu item and then open a folder. The following is what I have.

import SwiftUI

@main
struct Oh_My_App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(width: 480.0, height: 320.0)
        }.commands {
            CommandGroup(after: .newItem) {
                Button {
                    if let url = showFileOpenPanel() {
                        print(url.path)
                    }
                } label: {
                    Text("Open file...")
                }
                .keyboardShortcut("O")
            }
        }
    }
    
    func showFileOpenPanel() -> URL? {
        let openPanel = NSOpenPanel()
        openPanel.canChooseDirectories = true
        openPanel.canCreateDirectories = false
        openPanel.canChooseFiles = false
        openPanel.title = "Selecting a folder..."
        openPanel.message = "Please select a folder containing one or more files."
        let response = openPanel.runModal()
        return response == .OK ? openPanel.url : nil
    }
}

Okay. That's no problem. I can print the file path. Well, my actual question is how to return this value to ContentView? It is ContentView that is virtually running the show in this sample application. So I use ObservableObject as follows.

import SwiftUI

@main
struct Oh_My_App: App {
    @StateObject var menuObservable = MenuObservable()
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.commands {
            CommandGroup(after: .newItem) {
                Button {
                    menuObservable.openFile()
                } label: {
                    Text("Open file...")
                }
                .keyboardShortcut("O")
            }
        }
    }
}

class MenuObservable: ObservableObject {
    @Published var fileURL: URL = URL(fileURLWithPath: "")
    func openFile() {
        if let openURL = showFileOpenPanel() {
            fileURL = openURL
        }
    }
    
    func showFileOpenPanel() -> URL? {
        let openPanel = NSOpenPanel()
        openPanel.canChooseDirectories = true
        openPanel.canCreateDirectories = false
        openPanel.canChooseFiles = false
        openPanel.title = "Selecting a folder..."
        openPanel.message = "Please select a folder containing one or more files."
        let response = openPanel.runModal()
        return response == .OK ? openPanel.url : nil
    }
}
// ContentView.swift //
import SwiftUI

struct ContentView: View {
    @ObservedObject var menuObservable = MenuObservable()
    @State var filePath: String = ""
    
    var body: some View {
        ZStack {
            VStack {
                Text("Hello: \(filePath)")
            }.onChange(of: menuObservable.fileURL) { newValue in
                filePath = newValue.path
            }
        }
    }
}

My ContentView won't get updated. So how do I get ContentView to receive a value from the menu call from App? Thanks.


Solution

  • Right now, you're creating a new instance of MenuObservable in ContentView, so it doesn't have any connection to the instance that received the menu command. You need to pass a reference to your existing instance (ie the one owned by Oh_My_App).

    In your ContentView, change @ObservedObject var menuObservable = MenuObservable() to:

    @ObservedObject var menuObservable : MenuObservable
    

    And in your Oh_My_App:

    WindowGroup {
        ContentView(menuObservable: menuObservable)
    }