Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

22. Building a Mac App
Written by Sarah Reichelt

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

If you’ve worked through the previous chapters, you’ve already made several iOS apps. You may have used Catalyst to run an iOS app on your Mac, or perhaps you created a multi-platform iOS/macOS app. But in this chapter, you’ll write a purely Mac app. You’ll create a class of app that’s very common on Macs — a document-based app.

Many Mac apps are document-based. Think of apps like TextEdit, Pages, Numbers or Photoshop. Each document has its own window, and you can have multiple documents open at the same time.

In this chapter, you’ll build a Markdown editor. Markdown is a markup language that allows you to write formatted text quickly and easily. It can be converted into HTML for display but it’s much more convenient to write, read and edit.

You’ll create a document-based app from the Xcode template and see how much functionality that provides for free. Then you’ll go on to customize the file type for saving and opening as well as adding HTML preview, menus and a toolbar.

The Default Document App

Open Xcode and create a new project. Select macOS and choose Document App. Make sure the interface is SwiftUI and the language is Swift. Call the app MacMarkDown.

Once you’ve saved the project, build and run the app. If no windows open, select New from the File menu or if you see a file selector dialog, click New Document. You’ll see a single window showing some default text. You can edit this text and use the standard Edit menu commands for selection, cut, copy and paste as well as undo and redo.

Select Save from the File menu:

Saving the default document
Saving the default document

Note: If you don’t see the file extension in the save dialog, go to Finder ▸ Settings ▸ Advanced and turn on Show all filename extensions. This’ll make it easier to follow the next part of this chapter.

The default app uses a file extension of .exampletext, so choose a name and save your file with the suggested extension. Close the window and create a new window using Command-N. Now try opening your saved document by choosing Open… from the File menu.

And all this is without writing a single line of code!

Close the app, go back to Xcode and look at MacMarkDownApp.swift. Instead of the app body containing a WindowGroup as you have seen in other apps, it contains a DocumentGroup that has a newDocument argument set to an instance of MacMarkDownDocument. The ContentView gets a reference to this document.

Looking in ContentView.swift, you’ll see the only view inside the body is a TextEditor. This view allows editing long chunks of text. It is initialized with a text property that’s bound to the document’s text.

Open MacMarkDownDocument.swift to see where the file saving and opening happens. The first thing to note is the UTType extension. UT stands for Uniform Type and is the way macOS handles file types, file extensions and working out what apps can open what files. You’ll learn more about this in the next section when you customize the app to handle Markdown files.

In the MacMarkDownDocument structure, there’s a text property that holds the contents of the document and starts with the default text you saw in each new window. The readableContentTypes property sets what document types this app can open, taken from the UTType defined earlier.

The init and fileWrapper methods handle all the work of opening and saving the document files using the .exampletext file extension, but now it’s time to work out how to handle Markdown files.

Configuring for Markdown

When you double-click a document file on your Mac, Finder opens it with the default application: TextEdit for .txt files, Preview for .png files and so on. Right-click any document file and look at the Open With menu — you’ll see a list of the applications on your Mac that can open that type of file. Finder knows what apps to show because the app developers have specified what Uniform Types their app can handle.

Setting Document Types

Go to the project settings by selecting the project. That’s the item with the blue icon at the top of the Project navigator. Select the MacMarkDown target and choose the Info tab from the selection across the top.

Document type
Wurokewl tpfu

Imported type
Ilxocbas vzga

extension UTType {
  static var markdownText: UTType {
    UTType(importedAs: "net.daringfireball.markdown")
  }
}
static var readableContentTypes: [UTType] { [.markdownText] }

Testing the New Settings

Build and run the app. If there are any existing documents open, close them all and create a new document. Check that the default text is “# Hello MacMarkDown!”. Now, save the document and confirm that the suggested file name uses the .md file extension:

Saving with the Markdown extension
Kufilx sonn mpu Sikpsomg ezyekpouh

Markdown and HTML

Markdown is a markup language that uses shortcuts to format plain text in a way that converts easily to HTML. As an example, look at the following HTML:

<h1>Important Header</h1>
<h2>Less Important Header</h2>

<a href="https://www.kodeco.com">Kodeco</a>

<ul>
  <li>List Item 1</li>
  <li>List Item 2</li>
  <li>List Item 3</li>
</ul>
# Important Header
## Less Important Header

[Kodeco](https://www.kodeco.com)

- List Item 1
- List Item 2
- List Item 3

Converting Markdown to HTML

Back in Xcode, select the project in the Project navigator and this time, click the MacMarkDown project instead of the target. Go to the Package Dependencies tab and click the + button to add a new dependency. Enter this URL into the search field at the top right:

https://github.com/objecthub/swift-markdownkit
Find the package
Bagm llu huxzupa

Import the package
Islivs rgo hijbulo

import MarkdownKit
var html: String {
  let markdown = MarkdownParser.standard.parse(text)
  return HtmlGenerator.standard.generate(doc: markdown)
}

Adding the HTML Preview

The app needs a web view to display the HTML but SwiftUI doesn’t have a web view yet. However AppKit has WKWebView and you can use NSViewRepresentable to embed an AppKit view into a SwiftUI View.

// 1
import SwiftUI
import WebKit

// 2
struct WebView: NSViewRepresentable {
  // 3
  var html: String

  // 4
  func makeNSView(context: Context) -> WKWebView {
    WKWebView()
  }

  // 5
  func updateNSView(_ nsView: WKWebView, context: Context) {
    nsView.loadHTMLString(
      html,
      baseURL: Bundle.main.resourceURL)
  }
}

Displaying the HTML

To display the two views side-by-side in resizable panes, you’re going to embed the TextEditor and a WebView in an HSplitView. This is a macOS-specific SwiftUI view for exactly this purpose.

HSplitView {
  TextEditor(text: $document.text)
  WebView(html: document.html)
}
Sandbox setting
Gacwhum xabzizk

Web preview
Feb xhijiad

Framing the Window

When an app runs on an iPhone, it works out the available screen size and expands to fill it. The equivalent on macOS would be if every app ran in full screen mode and nobody wants that! But it does mean that you need to do more work to set frames for the views in your Mac apps.

.frame(minWidth: 200)
.frame(minWidth: 400, minHeight: 300)
Resizing the window
Wuneduqr gxa qohgoh

Adding a Settings Window

Nearly all Mac apps have a Settings window (previously called Preferences), so now you’ll add one to this app. Make a new SwiftUI View file and call it SettingsView.swift. Update body to look like this:

var body: some View {
  Text("Settings")
    .padding()
}
Settings {
  SettingsView()
}
Settings
Qeprugkf

Using AppStorage

SwiftUI uses property wrappers extensively to let us assign extra functionality to our properties, structures and classes. One of these property wrappers is for saving settings. If you’ve worked through the earlier chapters in this book, you’ll know all about @AppStorage, but for those of you who skipped straight to the macOS chapters (and who could blame you), here are the details.

Choosing a Font Size

You’ll add the ability to change the editor font size. In SettingsView.swift insert the following code inside the SettingsView structure but before body:

@AppStorage("editorFontSize") var editorFontSize: Int = 14
Stepper(value: $editorFontSize, in: 10 ... 30) {
  Text("Font size: \(editorFontSize)")
}
.frame(width: 260, height: 80)
.font(.system(size: CGFloat(editorFontSize)))
Font size
Jayt bado

Changing and Creating Menus

All Mac apps have a menu bar. Users expect to find your app supporting all the standard menu items, and it already does this. But it’s a nice touch to add your own menu items, not forgetting to give them keyboard shortcuts.

import SwiftUI

// 1
struct MenuCommands: Commands {
  var body: some Commands {
    // 2
    CommandGroup(before: .help) {
      // 3
      Button("Markdown Cheatsheet") {
        showCheatSheet()
      }
      // 4
      .keyboardShortcut("/", modifiers: .command)

      Divider()
    }

    // more menu items go here
  }

  // 5
  func showCheatSheet() {
    let cheatSheetAddress =
      "https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet"
    guard let url = URL(string: cheatSheetAddress) else {
      // 6
      fatalError("Invalid cheatsheet URL")
    }
    NSWorkspace.shared.open(url)
  }
}
.commands {
  MenuCommands()
}
Help menu
Xocy jaca

Adding a New Menu

Now it’s time to create your own menu. How about having the option to select different stylesheets for the web preview for your Markdown?

@AppStorage("styleSheet") var styleSheet: StyleSheet = .github
// 1
CommandMenu("Stylesheet") {
  // 2
  ForEach(StyleSheet.allCases, id: \.self) { style in
    // 3
    Button(style.rawValue) {
      styleSheet = style
    }
    // 4
    .keyboardShortcut(style.shortcutKey, modifiers: .command)
  }
}

Displaying the Styles

To make the web view use these styles, head over to WebView.swift and add the @AppStorage("styleSheet") property declaration to the WebView struct. The Markdown processor produces HTML text with no <head>, so to include the CSS file, you’re going to have to make the HTML a bit more complete.

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,                      // Changed line
    baseURL: Bundle.main.resourceURL)
}
Stylesheet
Smzguyvuoz

Creating a Toolbar

Right now, the app allows you to edit Markdown text and render the equivalent HTML in a web view. But it would be useful sometimes to see the actual HTML code. And if space is tight on a smaller screen, maybe it would be convenient to be able to turn off the preview completely.

enum PreviewState {
  case hidden
  case html
  case web
}
// 1
struct PreviewToolBarItem: ToolbarContent {
  // 2
  @Binding var previewState: PreviewState

  // 3
  var body: some ToolbarContent {
    // 4
    ToolbarItem {
      // 5
      Picker("", selection: $previewState) {
        // 6
        Image(systemName: "eye.slash")
        	.tag(PreviewState.hidden)
        Image(systemName: "doc.plaintext")
        	.tag(PreviewState.html)
        Image(systemName: "doc.richtext")
        	.tag(PreviewState.web)
      }
      .pickerStyle(.segmented)
      // 7
      .help("Hide preview, show HTML or web view")
    }
  }
}

Using the Toolbar

Now to attach the toolbar to ContentView. First, you need to add an @State property to hold the selected preview state. This is set to web by default:

@State private var previewState = PreviewState.web
.toolbar {
  PreviewToolBarItem(previewState: $previewState)
}
Toolbar
Geilsah

if previewState == .web {
  WebView(html: document.html)
    .frame(minWidth: 200)
}
var body: some View {
  HSplitView {
    TextEditor(text: $document.text)
      .frame(minWidth: 200)
      .font(.system(size: CGFloat(editorFontSize)))

    if previewState == .web {
      WebView(html: document.html)
        .frame(minWidth: 200)
    }
  }
  .frame(minWidth: 400, minHeight: 300)
  .toolbar {
    PreviewToolBarItem(previewState: $previewState)
  }
}
No preview
Zo dkebuej

Adding the HTML Text Preview

For the raw HTML display, add this after the previous if block:

else if previewState == .html {
  // 1
  ScrollView {
    // 2
    Text(document.html)
      .frame(minWidth: 200)
      .frame(
        maxWidth: .infinity,
        maxHeight: .infinity,
        alignment: .topLeading)
      .padding()
      // 3
      .font(.system(size: CGFloat(editorFontSize)))
      // 4
      .textSelection(.enabled)
  }
}
HTML preview
GDDH pvubouw

Markdown in AttributedStrings

SwiftUI has an AttributedString that can format Markdown. This isn’t directly relevant to this app, but since the app deals with Markdown, it seems appropriate to mention it.

var attributedString: AttributedString {
  // 1
  let markdownOptions =
  AttributedString.MarkdownParsingOptions(
    interpretedSyntax: .inlineOnly)
  // 2
  let attribString = try? AttributedString(
    markdown: document.text,
    options: markdownOptions)
  // 3
  return attribString ??
  AttributedString("There was an error parsing the Markdown.")
}
Text(attributedString)
AttributedString from Markdown
IkpnerohejHvreyb sqoj Waztgoyj

Installing the App

With an iOS App, when you build and run an app on your device, Xcode installs it on your iPhone or iPad and you can use it there, even after closing Xcode. For a Mac app, this isn’t quite as simple. Building and running doesn’t copy the app into your Applications folder but buries it deep within your Library.

Challenge: Add Another File Extension

When you set up the file types, you allowed the app to use either “.md” or “.markdown” for the file extensions. But some people use “.mdown” for Markdown files. Edit the project so that “.mdown” is a valid extension. To test it, rename one of your files to use this extension and see if you can open it in MacMarkDown.

Key Points

  • Apple provides a starting template for document-based Mac apps that can get you going quickly, but now you know how to customize this template to suit your own file types.
  • By setting up the file type for this app, you’ve made an app that can open, edit and preview any Markdown files, not just files created by this app.
  • Mac users expect all apps to work much the same way, with menus, toolbars, settings and multiple windows. Now you have the tools to make an app that does all these things.
  • And you have a Markdown editor you can use! The completed project is in the final folder for this chapter.

Where to Go From Here?

Well done! You made it through this chapter. You have made a document-based Mac app that you can use or extend and you have learned a lot about file types, Markdown and standard elements of Mac apps.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now