SwiftUI 2.0 — Commands (macOS Menu)

Published on

Get weekly handpicked updates on Swift and SwiftUI!

This article introduces how to add menus for the macOS platform in SwiftUI 2.

The current running environment is Xcode Version 12.0 beta (12A6159), macOS Big Sur 11.0 Beta (20A4299v).

Apple added the Multiplatform project template in SwiftUI 2.0, allowing the same set of code, with minimal adaptations, to meet the needs of both iOS and macOS. For apps running on macOS, having a custom menu is a very important platform feature. For me, who has no experience in macOS development, learning how to design and develop menus became very interesting and necessary.

Basics

Adding a menu to an app under SwiftUI is very convenient. The following code can add a basic menu to an app.

Swift
@main
struct CommandExampleApp: App {
    var body: some Scene {
        WindowGroup {
           ContentView()
        }
        .commands{
           Menu()
        }
    }
}

struct Menu: Commands {
    var body: some Commands {
        CommandMenu("My Menu") {
            Button("menu1") {
                print("menu1")
            }
        }
    }
}

Create a structure that conforms to the Commands protocol to describe the custom menu items you want to add in the app.

test1

Several Concepts

  • @CommandBuilder

    In SwiftUI 2.0, Apple brought us many new function builders. The body in Menu is actually a @CommandsBuilder. We can easily define the menus we need in a DSL manner using built-in statements. The advantage of DSL is its simplicity and clarity, but its functionality is somewhat limited. Currently, the methods supported by @CommandsBuilder are few and do not support logical judgment.

Swift
  struct MyCommand: Commands {
      // If multiple menu columns are needed, @CommandsBuilder must be explicitly declared before body, or use Group. This method is also applicable to other functionBuilder descriptions.
      @CommandsBuilder var body: some Commands {
          // Each CommandMenu is a menu column
          CommandMenu("Menu1") {
              Button("Test1") {
                print("test1")
              }
              Button("Test2") {
                print("test2")
              }
          }
          
          CommandMenu("Menu2") {
              Button(action: test1) {
                  Text("😃Button")
              }
          }
      }
      
      private func test1() {
          print("test command")
      }
  }
  • CommandMenu

    CommandMenu is a menu column. In the same CommandMenu, you can define multiple Buttons. The Content in CommandMenu conforms to the View protocol, meaning that many methods and controls in View can be used to describe the specific presentation of the menu. We can write menus like we write Views (e.g., setting fonts, colors, loops, judgments, etc.).

Swift
  CommandMenu("Menu") {
    Button("test1") {}
    Divider()
    Button(action: {}) { Text("test2").foregroundColor(.red) }
  }
  • Button

    The implementation method for individual menu items.

Swift
  Button(action: {}) {
    HStack {
        Text("🎱").foregroundColor(.blue)
        Divider().padding(.leading,10)
        Text("Button")
    }
  }
  • MenuButton

    The implementation method for submenus.

Swift
  CommandMenu("Test") {
         Button(action: { test1() }) {
              Text("test1").foregroundColor(.black)
         }
          
          #if os(macOS)
          MenuButton("Switch Selection") {
              Button("one") {
                  store.changeState(.one)
              }
              Button("two") {
                  store.changeState(.two)
              }
              Button("three") {
                  store.changeState(.three)
              }
          }
          #endif
      }
  • .commands

    A Scene method to add menus. Multiple menus conforming to the Commands protocol can be added in commands. All defined menus will be displayed together.

Swift
      WindowGroup {
          RootView()
              .environmentObject(store)
      }
      .commands {
          OtherMenu()
          Menu()
      }
  • keyboardShortcut

    Add keyboard shortcuts to menu items. Set the required keys with modifiers, and use .help to add hover help for the option.

    Swift
    Button(action:{test1()})
    {
        Text("test1").foregroundColor(.black)
    }
    .keyboardShortcut("1", modifiers: [.command,.shift])
    .help("help test1")
                
    Button("test2", action: test2)
    .keyboardShortcut("2", modifiers: .command)
  • CommandGroup

    Add custom features to the default menu options provided by the system. Decide whether to replace the original option or set it before or after a specified option position using replacing, before, after.

    Swift
    // Add your own option under the system's preset Help menu
    CommandGroup(replacing: CommandGroupPlacement.appInfo, addition: {Button("replace"){}})
    CommandGroup(before: CommandGroupPlacement.help, addition: {Button("before"){}})
    CommandGroup(after: CommandGroupPlacement.newItem, addition: {Button("after"){}})

Example

This simple example demonstrates how to use the store to influence the behavior of an app through the menu, with simple multi-platform adaptation. On the macOS platform, different options in the submenu affect the display text. On iOS, this is implemented with a picker.

Full code can be downloaded here

Swift
// [Swift code demonstrating the implementation of commands and menus in a SwiftUI app]

Supplement (Opening New Windows)

I tried to open a new View in the menu button, but did not find a native SwiftUI method. I would prefer if @SceneBuilder could support logical judgments, so I could organize the View I want to display in the WindowGroup as I wish.

Swift
// Open a new View
Button("other window"){
    // [Code to create and display a new window in a SwiftUI app]
}

// Open the system file selection panel
Button("open panel"){
    // [Code to open a system file selection panel in a SwiftUI app]
}

Current Issues

Since it’s still in the early testing phase, there are some shortcomings in the implementation and functionality of the menu. The following are the issues I am currently more concerned about:

  • The default color of the Button text is different from the system menu option color. It needs to be set manually.
  • The color of ShortCut is different from the system menu color.
  • The color of Divider is different from the system menu.
  • MenuButton requires compilation comments for multi-platform development, while others like creating Commands, .commands, etc., do not.
  • @CommandBuilder and @SceneBuilder currently do not support judgments. Therefore, it is impossible to dynamically add or reduce a menu column through the program. However, since multiple Commands structures can be added through .command, it is expected that there will be such plans in the future.
Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed