Swiftui: Changing Default Command Menus on Macos

SwiftUI: Changing default Command Menus on macOS

Use CommandGroup, which has init options to append or replace existing menus:

.commands {
CommandGroup(before: CommandGroupPlacement.newItem) {
Button("before item") {
print("before item")
}
}

CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("Custom app info") {
// show custom app info
}
}

CommandGroup(after: CommandGroupPlacement.newItem) {
Button("after item") {
print("after item")
}
}
}

Nice tutorial: https://swiftwithmajid.com/2020/11/24/commands-in-swiftui/

SwiftUI: How to implement Edit menu in macOS app

Have a look at the commands modifier, the CommandGroup and CommandMenu.

@main
struct MyApp: App {

var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// for example
CommandGroup(replacing: .help) {
Button(action: {someActionHere()}) {
Text("MyApp Help")
}
}
CommandMenu("Edit") {
// ...
}
}
}
}

Hiding Edit Menu of a SwiftUI / MacOS app

To my knowledge you cannot hide the whole menu, you can just hide element groups inside of it:

    .commands {
CommandGroup(replacing: .pasteboard) { }
CommandGroup(replacing: .undoRedo) { }
}

SwiftUI macOS Commands (menu bar) and View

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



Related Topics



Leave a reply



Submit