8

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.

1 Answers1

10

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

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thank you very much for your answer. Best answer I have ever received on stack overflow! SwiftUI and macOS are all very new to me and I still have 2 questions – Tertuliano Máximo Afonso Mar 10 '21 at 02:37
  • AppState holds the state for many views right? How can I change the state of the foreground view? For example I have a state that hide/show some elements of it. But I can have multiple windows of view. How can when I click the button toggle the hide/show of the focused view? – Tertuliano Máximo Afonso Mar 10 '21 at 02:38
  • The second question is also about the foreground view. How to call a function instead of changing state? For example, I have a button in my view that does X() and the X() code is in the view struct (since it deals with things of that view, but should it be elsewhere?) how do I create a menu item that calls X()? – Tertuliano Máximo Afonso Mar 10 '21 at 02:41
  • Regarding your second question, that's what I tried to address in my initial answer. I think you need to think about what happens in `X()` -- it probably changes some state, right? For your first question, you will base the state of your view on what is going on in AppState. See my updated answer. – jnpdx Mar 10 '21 at 03:32
  • X() copies something based on the view controls (that is the views states not related to the app state) to the pasteboard – Tertuliano Máximo Afonso Mar 10 '21 at 03:56
  • About the first question: I was able to hide/show using the menu bar. Thanks! But the problem is that it changes all views and I want it to change only the foreground view. For example in Preview when you change to view Double page instead os Single page it only affects the foreground preview window. – Tertuliano Máximo Afonso Mar 10 '21 at 04:04
  • Also related to the CommandMenu and SwiftUI. Do you know how I remove items from the View menu ("Show tab bar", "Show all tabs"...)? – Tertuliano Máximo Afonso Mar 10 '21 at 04:06
  • See my update for how to handle a copy operation like you discussed. Look up @FocusedValue/@FocusedBinding for keeping track of a focused field in the front window. Besides that, you may have to do some management yourself about keeping track of which window is active. I believe I've answered as much as I can about the original question. If you have details about secondary or more detailed answers, it might be good to start another question. – jnpdx Mar 10 '21 at 04:15
  • You unaccepted because my answer and updates didn’t cover a use case you didn’t mention in the initial question? – jnpdx Mar 10 '21 at 04:36
  • The initial question was not how to change the state from a view when the menu item is clicked but how to call a function from the view. You did answer that after updating with your ```customCopy``` but I want to know if there are alternatives to your method on how to do it, that is why I unaccepted the answer. I will probably accept it if no one else answers it. Thank you very much for your time ;) – Tertuliano Máximo Afonso Mar 10 '21 at 11:31
  • 1
    @jnpdx The only problem is that on macOS when you have multiple windows of the same app, then changing the view state changes the view state in all windows. It is fine to share model data across the scenes, but the scene / view states needs to be independent. Is there a solution for that? – user1046037 Oct 10 '21 at 01:03
  • Thank you! I've been trying to wrap my head around StateObject, ObservedObject & PublishedObject and this is the cleanest example I've seen. – pizzafilms Feb 11 '23 at 00:12