Chapters

Hide chapters

macOS by Tutorials

First Edition · macOS 12 · Swift 5.5 · Xcode 13

Section I: Your First App: On This Day

Section 1: 6 chapters
Show chapters Hide chapters

11. Adding Menu Controls
Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you created a document-based app. You imported a package to convert Markdown to HTML, and you added controls to allow the user to select a preview style. You even included an AppKit view in the SwiftUI app.

In this chapter, you’ll dive deeply into menus. In Section 1, you added menus to your app, so some of this is familiar to you. But those menus applied app-wide settings only. Now you’ll learn some different menu tricks, as well as how to track the active window so you can apply menu actions to that window only.

Adding the Style Files

The web view renders the HTML using a default style, but it’d be nice to have the ability to choose your own styles by using Cascading Style Sheets (CSS).

Open your project from the last chapter or open the starter project in the downloaded materials for this chapter.

Next, open the assets folder in this chapter’s downloads and locate the StyleSheets folder.

Drag this folder into your Project navigator, selecting Copy items if needed, Create groups and the MarkDowner target:

Adding the StyleSheets folder.
Adding the StyleSheets folder.

This new folder contains a set of CSS files, with some different styles, and a .swift file that sets up an enumeration for these styles. You’ll use the data in this enumeration to generate menu items.

Creating a New Menu

You’re going to have a lot of menu code, so to hold it all, create a new Swift file called MenuCommands.swift. Replace its contents with:

// 1
import SwiftUI

// 2
struct MenuCommands: Commands {
  // 3
  var body: some Commands {
    // 4
    EmptyCommands()
  }
}
@AppStorage("styleSheet")
  var styleSheet: StyleSheet = .raywenderlich

Generating Menu Items

Next, you’re going to use the enumeration to create the items for this new menu. Replace EmptyCommands() with:

// 1
CommandMenu("Display") {
  // 2
  ForEach(StyleSheet.allCases, id: \.self) { style in
    // 3
    Button {
      // 4
      styleSheet = style
    } label: {
      // 5
      Text(style.rawValue)
    }
    // keyboard shortcut goes here
  }
  
  // more menu items
}

// more menus
.commands {
  MenuCommands()
}
Display menu
Jaqkhar bifu

Styling the HTML

To get your WebView to use the selected style, open WebView.swift.

@AppStorage("styleSheet")
  var styleSheet: StyleSheet = .raywenderlich
var formattedHtml: String {
  return """
    <html>
    <head>
       <link href="\(styleSheet).css" rel="stylesheet">
    </head>
    <body>
       \(html)
    </body>
    </html>
    """
}
func updateNSView(_ nsView: WKWebView, context: Context) {
  nsView.loadHTMLString(
    formattedHtml,
    baseURL: Bundle.main.resourceURL)
}
Changing styles
Fkuvfenf tqwdut

Adding Keyboard Shortcuts

Back in Section 1, you added a menu that used a Picker. This gave you a checkmark beside the selected choice, but didn’t allow for keyboard shortcuts. Since this is an editor app, it’s more important that users can keep their hands on the keyboard, so having keyboard shortcuts is the priority.

.keyboardShortcut("t", modifiers: .command)
.keyboardShortcut(KeyEquivalent(style.rawValue.first!))
Style menu shortcuts
Mfbsa xeca mnalgfupt

.foregroundColor(style == styleSheet ? .accentColor : .primary)
Active style sheet marked
Exlogo dwtpe rjeew pafruc

Inserting a Submenu

Another useful feature would be the ability to change the editor’s font size. You’ll add this to the Display menu as a submenu, with items to increase, decrease and reset the font size.

@AppStorage("editorFontSize") var editorFontSize: Double = 14
// 1
Divider()

// 2
Menu("Font Size") {
  // 3
  Button("Smaller") {
    if editorFontSize > 8 {
      editorFontSize -= 1
    }
  }
  // 4
  .keyboardShortcut("-")

  // 5
  Button("Reset") {
    editorFontSize = 14
  }.keyboardShortcut("0")

  Button("Larger") {
    editorFontSize += 1
  }.keyboardShortcut("+")
}
@AppStorage("editorFontSize") var editorFontSize: Double = 14
.font(.system(size: editorFontSize))
Font size submenu
Vixq kuje habjabi

Using the Help Menu

Back in Section 1, you removed the Help menu item as it didn’t do anything useful. In this app, you’ll make it show a new window that actually provides some useful information.

// 1
CommandGroup(replacing: .help) {
  // 2
  NavigationLink(
    // 3
    destination:
      WebView(
        html: nil,
        address: "https://bit.ly/3x55SNC")
      // 4
      .frame(minWidth: 600, minHeight: 600)
  ) {
    // 5
    Text("Markdown Help")
  }
}
Opening a help window.
Opesotk o qitn vidreg.

Focusing on a Window

So far, all the menu items you’ve added apply their actions to the entire app. But this is a document-based app, so you’ll want to direct some menu items to the active window only. How can you tell which is the active window?

https://github.com/LostMoa/KeyWindow
Finding the KeyWindow package.
Mifnujc qyo ZazXalqel jijlebe.

Package dependencies
Mazruxi yigavjuxxeav

Configuring the Library

Start by opening MarkDownerDocument.swift and importing the new package by adding this line at the top of the file:

import KeyWindow
extension MarkDownerDocument: KeyWindowValueKey {
  public typealias Value = Binding<Self>
}
import KeyWindow
ContentView(document: file.$document)
  .observeWindow()
.keyWindow(
  MarkDownerDocument.self,
  $document)

Adding a Window-specific Menu

In MenuCommands.swift, add the following at the top to import the library:

import KeyWindow
@KeyWindowValueBinding(MarkDownerDocument.self)
  var document: MarkDownerDocument?
Code folding
Joca zaptarj

// 1
CommandMenu("Markdown") {
  // 2
  Button("Bold") {
    // 3
    document?.text += "**BOLD**"
  }
  // 4
  .keyboardShortcut("b")

  // 5
  Button("Italic") {
    document?.text += "_Italic_"
  }.keyboardShortcut("i", modifiers: .command)

  Button("Link") {
    let linkText = "[Title](https://link_to_page)"
    document?.text += linkText
  }

  Button("Image") {
    let imageText = "![alt text](https://link_to_image)"
    document?.text += imageText
  }
}
Markdown menu
Zesnbuds rete

Exporting the HTML

You can type Markdown and preview this as HTML, but you may want to export the HTML code for use on a web site.

// 1
CommandGroup(after: .importExport) {
  // 2
  Button("Export HTML…") {
    // exportHTML()
  }
  // 3
  .disabled(document == nil)
}
Export menu item
Umjuqc duxo uzel

func exportHTML() {
  // 1
  guard let document = document else {
    return
  }

  // 2
  let savePanel = NSSavePanel()
  savePanel.title = "Save HTML"
  savePanel.nameFieldStringValue = "Export.html"

  // 3
  savePanel.begin { response in
    // 4
    if response == .OK, let url = savePanel.url {
      // 5
      try? document.html.write(
        to: url,
        atomically: true,
        encoding: .utf8)
    }
  }
}
Save panel
Caki nunoz

Exported HTML in Safari
Olbucwor JTYD ep Poboma

Coding for the Touch Bar

A lot of MacBooks have a touch bar, and it’s frequently used to provide auto-complete suggestions or formatting options. So, it would be a neat touch to have some of your Markdown menu items available in the touch bar too.

Touch bar simulator
Tiemn fup ketedabap

.touchBar {
  TouchbarCommands()
}
Touch bar commands
Tiiyn yor vanqowpv

Challenges

Challenge 1: Keyboard shortcuts

When you added the Display menu, you created keyboard shortcuts for every menu item. Since then, you’ve added new menu items to the File and Help menus and they have no shortcuts. You also created a Markdown menu and only some of the items there have shortcuts.

Challenge 2: More Markdown snippets

The Markdown menu has a few snippets, but it’d be good to have more. Add a Headers submenu that inserts the various header types. Header 1 starts with 1 #, Header 6 starts with 6 #s. You can use the header level number as the shortcut.

Key Points

  • The default macOS document app gets a standard suite of menus, but you can add to them in lots of useful ways.
  • An enumeration can automate the creation of a menu. This can even extend to generating keyboard shortcuts.
  • Include a Menu in a menu item to create a submenu.
  • Using a NavigationLink as a menu item allows you to open a new window containing any SwiftUI view.
  • Keeping track of the frontmost window isn’t an easy task and Apple’s mechanisms don’t always work.
  • Once you know which window is the active one, you can target it directly from menu items. This allows you to have window-specific menu items.
  • When saving a file, use an NSSavePanel to request the save path from the user. When you ask the user to select a file, your app can write to that file, even though it’s outside the app’s sandbox container.
  • A lot of MacBooks have a touch bar and SwiftUI makes it easy to add to the default touch bar controls.

Where to Go From Here?

Well done! You’ve reached the end of another section, and you’ve completed a new app. If you’ve been working through the book in order, you’ve now got three working apps in three very different styles.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now