SwiftUI macOS Commands (menu bar) and View

Hi I am starting to learn SwiftUI and macOS development. I am using the SwiftUI life cycle. How do I call a function from the focused window from the menu bar.

Besides Apple documentation, I found this reference and am able to create menu items using Commands but I have no idea how to call a function from my view.

For example:

Suppose this is my App struct:

import SwiftUI

@main
struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }.commands {
        CommandMenu("First menu") {
            Button("Action!") {
                // How do I call the views action function?
            }
        }
    }
}

and this is my View:

struct ContentView: View {
    public func action() {
        print("It works")
    }
    var body: some View {
        Text("Example")
    }
}

I just typed the example code sorry if there are any typos but I hope you can get the idea.

Upvotes: 10

Views: 6633

Answers (1)

jnpdx
jnpdx

Reputation: 52347

Because Views in SwiftUI are transient, you can't hold a reference to a specific instance of ContentView to call a function on it. What you can do, though, is change part of your state that gets passed down to the content view.

For example:

@main
struct ExampleApp: App {
    @StateObject var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView(appState: appState)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }.commands {
            CommandMenu("First menu") {
                Button("Action!") {
                    appState.textToDisplay = "\(Date())"
                }
            }
        }
    }
}

class AppState : ObservableObject {
    @Published var textToDisplay = "(not clicked yet)"
}

struct ContentView: View {
    @ObservedObject var appState : AppState
    
    var body: some View {
        Text(appState.textToDisplay)
    }
}

Note that the .commands modifier goes on WindowGroup { }

In this example, AppState is an ObservableObject that holds some state of the app. It's passed through to ContentView using a parameter. You could also pass it via an Environment Object (https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views)

When the menu item is clicked, it sets textToDisplay which is a @Published property on AppState. ContentView will get updated any time a @Published property of AppState gets updated.

This is the general idea of the pattern you'd use. If you have a use case that isn't covered by this pattern, let me know in the comments.

Updates, based on your comments:

import SwiftUI
import Combine

@main
struct ExampleApp: App {
    @StateObject var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView(appState: appState)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }.commands {
            CommandMenu("First menu") {
                Button("Action!") {
                    appState.textToDisplay = "\(Date())"
                }
                Button("Change background color") {
                    appState.contentBackgroundColor = Color.green
                }
                Button("Toggle view") {
                    appState.viewShown.toggle()
                }
                Button("CustomCopy") {
                    appState.customCopy.send()
                }
            }
        }
    }
}

class AppState : ObservableObject {
    @Published var textToDisplay = "(not clicked yet)"
    @Published var contentBackgroundColor = Color.clear
    @Published var viewShown = true
    
    var customCopy = PassthroughSubject<Void,Never>()
}

class ViewModel : ObservableObject {
    @Published var text = "The text I have here"
    var cancellable : AnyCancellable?

    func connect(withAppState appState: AppState) {
        cancellable = appState.customCopy.sink(receiveValue: { _ in
            print("Do custom copy based on my state: \(self.text) or call a function")
        })
    }
}

struct ContentView: View {
    @ObservedObject var appState : AppState
    @State var text = "The text I have here"
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Text(appState.textToDisplay)
                .background(appState.contentBackgroundColor)
            if appState.viewShown {
                Text("Shown?")
            }
        }
        .onReceive(appState.$textToDisplay) { (newText) in
            print("Got new text: \(newText)")
        }
        .onAppear {
            viewModel.connect(withAppState: appState)
        }
    }
}

In my updates, you can see that I've addressed the question of the background color, showing hiding a view, and even getting a notification (via onReceive) when one of the @Published properties changes.

You can also see how I use a custom publisher (customCopy) to pass along an action to ContentView's ViewModel

Upvotes: 12

Related Questions