Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

21. Building a Mac App
Written by Sarah Reichelt

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

Many Mac apps are document-based. Think of apps like TextEdit, Pages, Numbers or Photoshop. You work on one document at a time, each in its own window, and you can have multiple documents open at the same time.

In this chapter, you’re going to 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 displaying but is much more convenient to write and edit than HTML.

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 the HTML preview, a toolbar and menus.

The default document app

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

Once you have 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 ▸ Preferences ▸ 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’ll have seen in other apps, it contains a DocumentGroup which has a newDocument parameter that is set to an instance of MacMarkDownDocument. The ContentView is passed a reference to this document.

If you look in ContentView.swift you’ll see that the only view inside the body is a TextEditor. This view allows editing long chunks of text. It has a text property which is 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 struct, there is a text property that holds the contents of the document and is initialized with the default text you saw in each new window when you ran the app. 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.

Setting up the app for Markdown

When you double-click a document file on your Mac, Finder will open it with the default application: TextEdit for .txt files, Preview for .png files and so on. And if you right-click any document file and look at the Open With menu, you’ll see a list of the applications on your Mac that are able to open that type of file. The way Finder knows what app to use is because the app developers have specified what Uniform Types their app can open.

To set up a document-based app to open a particular file type, you’ll need three pieces of data:

  • The Uniform Type Identifier or UTI.

  • What standard file type this conforms to.

  • The file extension or extensions.

Apple provides a list of system-declared uniform types at https://apple.co/3iSjUwz which can often be useful when working out file types for an app, but in this case it doesn’t help as Markdown isn’t on the list.

However searching for “markdown uniform type” will get you to https://daringfireball.net/linked/2011/08/05/markdown-uti, where John Gruber, the inventor of Markdown, says that the Uniform Type Identifier should be “net.daringfireball.markdown” and that this conforms to “public.plain-text”.

Searching for “markdown” at https://fileinfo.com/extension/markdown, you can see that the most popular file extensions for Markdown are “.md” and “.markdown”.

Armed with this information, you’re ready to switch your app from working with plain text with the extension .exampletext, to working with Markdown text with the extensions of .md or .markdown.

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 list. Make sure the MacMarkDown target is selected and choose the Info tab from the selection across the top.

Expand the Document Types section and change the Identifier to “net.daringfireball.markdown”.

Document type
Document type

Next, expand the Imported Type Identifiers section and make the following changes:

Description: Markdown Text

Identifier: net.daringfireball.markdown

Extensions: md, markdown

All the other settings can stay the same as the Conforms To field already contains “public.plain-text”.

Imported type
Imported type

Now there is only one more place to make changes before your app can save and open Markdown files. Go back to MacMarkDownDocument.swift and replace the UTType extension with this:

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

This creates a new UTType called markdownText that uses the Uniform Type Identifier you just entered.

Inside the struct, change readableContentTypes to use this new type:

static var readableContentTypes: [UTType] { [.markdownText] }

And just for fun, change the default text in init to “# Hello, MacMarkDown!” which is the Markdown format for a level 1 header.

Testing the new settings

Build and run the app. If there were 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 is using the .md file extension.

Saving with the Markdown extension
Saving with the Markdown extension

Save and close the document window and then find the file in Finder and right-click it to show its Open With menu. You’ll see MacMarkDown listed there because your settings told the Finder that your app could open Markdown files. If you have any Markdown files created by another app, you’ll be able to open them in MacMarkDown too.

Phew! That was a dense section with a lot of detail, but now you have a document-based app that saves and opens Markdown files. In the next sections, you’ll learn more about Markdown and add a preview ability to your app.

Markdown and HTML

Markdown is 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.raywenderlich.com">Ray Wenderlich</a>

<ul>
  <li>List Item 1</li>
  <li>List Item 2</li>
  <li>List Item 3</li>
</ul>

To write the same in Markdown, you can use:

# Important Header
## Less Important Header

[Ray Wenderlich](https://www.raywenderlich.com)

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

I think you’ll agree that the Markdown version is easier to write and more likely to be accurate.

You can find out more about Markdown from this very helpful cheatsheet: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet.

In MacMarkDown, you write text using Markdown. The app will convert it to HTML and display it to the side in a web view.

Swift doesn’t have a built-in Markdown converter, so the first thing is to import a Swift Package to do this. The one you’re going to use in this app is https://github.com/objecthub/swift-markdownkit.

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 plus button to add a new dependency. Enter this URL: https://github.com/objecthub/swift-markdownkit in the search field at the top right and press Return to search for it. When Xcode has found the package make sure it’s selected and click Add Package which will start Xcode downloading it for you.

Find the package
Find the package

Once the download is complete, you’ll see a new dialog asking you what parts of the package you want to use. Choose the MarkdownKit Library and click Add Package to import it into your project.

Import the package
Import the package

The next step is to edit MacMarkDownDocument.swift so it can create an HTML version of the document. To use the package you just added, you need to put import MarkdownKit at the top of the file.

import MarkdownKit

Under where the text property is defined, define an html property:

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

This code creates a computed property that uses MarkdownKit’s MarkdownParser to parse the text and its HtmlGenerator to convert it into HTML.

Your document now has two properties. One is the plain text and that is what is saved with each document file. The other is the HTML version of that plain text which is derived from the text using the MarkdownKit package.

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 WKWebView into a SwiftUI View.

Create a new Swift file called WebView.swift and replace its contents with this code:

// 1
import SwiftUI
import WebKit

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

  init(html: String) {
    self.html = html
  }

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

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

Stepping through this:

  1. The SwiftUI library is needed to use NSViewRepresentable and WebKit is needed for WKWebView.
  2. WebView will be the name of the SwiftUI view that this struct defines. It conforms to the NSViewRepresentable protocol which provides a bridge between AppKit’s NSViews and SwiftUI Views.
  3. This struct only needs one String property to store the HTML text.
  4. NSViewRepresentable has two required methods: makeNSView creates and returns the NSView, in this case a WKWebView.
  5. The second required method is updateNSView which is called whenever there is a change to the properties that requires a view update. In this case, every time the HTML changes, the web view will reload the HTML text.

Now it’s time to display this web view, so head over to ContentView.swift which must be feeling rather abandoned. Usually it gets a lot more attention in a SwiftUI app!

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 a HSplitView. This is a macOS-specific SwiftUI view for exactly this purpose.

Replace the contents of body with this:

HSplitView {
  TextEditor(text: $document.text)
  WebView(html: document.html)
}

TextEditor has a binding to document.text as indicated by the $. This means that it can make changes to document.text which will flow back to the document. WebView doesn’t makes changes, it only displays, so it doesn’t need a binding.

Don’t run yet, there is one more setting you need to change. Mac apps run in a sandbox by default. You can turn this off, but if you plan to put your app on the Mac App Store, sandboxing is essential, and it’s a good idea generally as a protection for your app and your Mac. But the standard settings block web views from loading anything, even local data.

Go to the project settings and select the MacMarkDown target. Click the Signing & Capabilities tab.

Now you can check Outgoing Connections (Client) which will allow your WebView to load content.

Sandbox setting
Sandbox setting

Build and run the app.

Web preview
Web preview

Type in some Markdown and see the HTML appear in the side panel. Try dragging the divider bar left or right and test resizing the window. It looks like some size restrictions would be a good idea.

Framing the window

When an app is running on an iPhone or iPad, it can work out the available screen size and expand 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.

In this case, you want the TextEditor filling the left side of the window and the WebView filling the right side. They should both resize as the user resizes the window and as the user drags the divider between them. But the divider should never allow either view to disappear and the window should have a minimum size.

Back in ContentView.swift, add this frame modifier to both the TextEditor and the WebView, which will make sure they can never get narrower than 200:

.frame(minWidth: 200)

And add this frame modifier to the HSplitView:

.frame(minWidth: 400, idealWidth: 600, maxWidth: .infinity,
       minHeight: 300, idealHeight: 400, maxHeight: .infinity)

This sets the minimum and ideal size for the window but allows it to be expanded as much as possible. The minimum width will allow both panes to fit at their minimum widths.

Build and run again and try resizing each pane and the window. That’s better. :]

Resizing the window
Resizing the window

Adding a settings window

Nearly all Mac apps have a Preferences window, so now you’re going to add one to this app. Make a new SwiftUI View file and call it SettingsView.swift. Update the body to look like this:

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

This changes the default “Hello, world” text to say “Settings” and adds a padding() modifier, so that when you run the app, you can confirm that the correct view is being displayed.

Now it’s time to configure the app to show this view as the preferences view.

Open MacMarkDownApp.swift. Inside the body after DocumentGroup add these lines:

Settings {
  SettingsView()
}

It’s always important to explain complex chunks of code, so here is what this code is doing:

  • Create a Preferences… menu item in the app’s File menu.
  • Add the standard keyboard shortcut: Command-Comma.
  • Set up a preferences window titled “MacMarkDown Preferences”.
  • Configure the preferences window to display SettingsView.
  • Establish window controls so that only one copy of this window is ever created and trying to open Preferences again if the window is already displayed will just bring it to the front.

Not bad for what could be a single line of code. :]

Build and run the app, then select Preferences… from the File menu or type Command-Comma and your Settings view will appear. It’s really small - just large enough to hold the text “Settings” but it’s there and now you can edit it.

Settings
Settings

@AppStorage

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

Previously, you may have used UserDefaults which works really well for storing small chunks of user data like preference settings, but you have to keep them in sync manually. If a UI element changes a setting, you have to write to UserDefaults. If you’re displaying the UI, maybe you need to read from UserDefaults to set the state of a checkbox or to implement the choices made. And what if a setting changes after the display has been drawn?

The@AppStorage property wrapper makes all this so much easier. Under the hood, it’s still using UserDefaults but it handles a lot of these details.

Choosing a font size

You’re going to add the ability to change the editor font size. In SettingsView.swift add the following code inside the SettingsView struct but before body:

@AppStorage("editorFontSize") var editorFontSize: Int = 14

This is only one line, but it packs in a lot of functionality.

  • @AppStorage sets up this variable to use the AppStorage property wrapper.

  • The text in brackets assigns the name of the UserDefaults setting.

  • Then the variable is defined as usual, with a type and a default value. It’s neater if you use the same name for the UserDefaults property and the variable, but this isn’t strictly necessary.

Now for some UI to change this setting. Replace the default body contents with this:

Stepper(value: $editorFontSize, in: 10 ... 30) {
  Text("Font size: \(editorFontSize)")
}
.frame(width: 260, height: 80)

To apply this setting, go back to ContentView.swift and add the same @AppStorage line to the top of the struct. This will mean that the ContentView is able to access this setting even if the preferences window has never been opened and will react to any changes to it.

Add a font modifier to the TextEditor:

.font(.system(size: CGFloat(editorFontSize)))

Now build and run the app again. Make sure there is some text in the editor so you can see it change. Open the Preferences window and use the arrows to change the font size. The editor font size will automatically change as you change the setting.

Font size
Font size

Make a new window so you have more than one open at the same time. Confirm that the font size change is applied to both windows. And if you quit and restart the app, your font size setting is preserved.

Changing and creating menus

All Mac apps have a menu bar. Users will 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.

SwiftUI provides two ways to add new menu items. You can use a CommandMenu to insert a completely new menu. Or you can use a CommandGroup to add menu items to an existing menu. Both of these are applied by adding a commands modifier to the DocumentGroup.

You can include the contents of the commands modifier directly in place in MacMarkDownApp.swift but since menu definitions can get quite extensive, it makes your project easier to read if you separate them out into their own file. Create a new Swift file called MenuCommands.swift and replace the contents with this:

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 will 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)
  }
}

So what’s happening here?

  1. Menu content and its body must conform to the Commands protocol so that you can use this struct to set the menu commands for your app.
  2. A CommandGroup has to be positioned either before, after or in place of existing menu items. Check out the docs for CommandGroupPlacement to see which menu items have identifiers that you can use.
  3. Here you’re adding a Button as a menu item with a Divider below it to make the menu look better.
  4. The button has a keyboard shortcut of Command-/.
  5. The menu item button is calling a function that will open a URL in the default browser.
  6. If a web address has been typed in wrongly, it’s better to catch it in development with a fatal error instead of hiding the mistake in a guard statement.

To make this new menu item appear, go to MacMarkDownApp.swift and add this modifier to DocumentGroup:

.commands {
  MenuCommands()
}

Build and run the app, then look at the Help menu. Select the new menu item or type Command-/ to open the cheatsheet in your browser.

Help menu
Help menu

Adding a new menu

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

Open the assets folder in the downloads for this chapter and find the StyleSheets folder. Drag this folder into your Project navigator, making sure that Copy items if needed is checked, Create groups is selected and that the folder is being added to the target. This folder contains a small collection of CSS files plus a Swift file containing an enum listing these styles.

To display these in a menu, go back to MenuCommands.swift and add this property:

@AppStorage("styleSheet")
  var styleSheet: StyleSheet = .raywenderlich

This creates a new @AppStorage property for a StyleSheet and sets it to use the Ray Wenderlich style as the default.

Replace the // more menu items will go here comment with this:

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

Here is what this code is doing:

  1. To create an entirely new menu, use CommandMenu giving it the title of the new menu.
  2. Loop through all the cases in the StyleSheet enum.
  3. Each style has a menu item button with the title set to the rawValue string for the case. These buttons change the styleSheet property.
  4. A keyboard shortcut is set for each one using a key equivalent set up in the enum.

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.

Add this computed property to WebView:

var formattedHtml: String {
  return """
      <html>
      <head>
         <link href="\(styleSheet).css" rel="stylesheet">
      </head>
      <body>
         \(html)
      </body>
      </html>
      """
}

This uses multi-line string syntax to wrap the html and styleSheet properties into an HTML document. Because you set the app’s Bundle.main.resourceURL as the web view’s baseURL, a direct link to the CSS files will work.

In updateNSView(_:context) replace html with formattedHtml.

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

Build and run the app. Use your new menu to change to a different stylesheet.

Stylesheet
Stylesheet

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 being generated. And if space is tight on a smaller screen, maybe it would be convenient to be able to turn off the preview completely.

So now you’re going to add another UI element that is very common in Mac apps - the toolbar. In the toolbar, you’ll add controls to switch between three preview modes: web, HTML and off.

A toolbar is added as a modifier to a view, in this case the ContentView. It can be added in the same file, but as you did with the menu contents, you’re going to put this in its own file. Create a new Swift file and name it ToolbarCommands.swift. Open the new file and change the import line to import SwiftUI.

import SwiftUI

You’re adding the ability to switch between three states so this seems like a good use case for an enum. In ToolbarCommands.swift insert this:

enum PreviewState {
  case hidden
  case html
  case web
}

And then add this struct:

// 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(SegmentedPickerStyle())
      // 7
      .help("Hide preview, show HTML or web view")
    }
  }
}

This looks like a lot, but take it one step at a time.

  1. So that this struct can be assigned as the content of a Toolbar, it must conform to the ToolbarContent protocol.
  2. A binding variable receives the selected preview state from the parent view and passes any changes back to it.
  3. The body also needs to conform to the ToolbarContent protocol.
  4. ToolbarContent views must be either ToolbarItem or ToolbarItemGroup. As this is only showing a single view, a ToolbarItem is the right one to use.
  5. Since this is going to switch between three possibilities, a segmented picker is a good UI choice.
  6. Each segment displays an SF Symbol image and is tagged with the corresponding PreviewState case. Apple’s SF Symbols app shows all these icons so you can search for an appropriate one.
  7. The help modifier provides a tooltip and accessibility text.

Using the toolbar

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

@State private var previewState = PreviewState.web

Next, add a toolbar modifier to the HSplitView after the frame modifier:

.toolbar {
  PreviewToolBarItem(previewState: $previewState)
}

This creates a toolbar and sets its content to the PreviewToolbarItem you just created, passing in a binding to the previewState variable so that changes can be passed back.

Build and run the app to see a toolbar with these three options at the far right. You can click each one and see the visual differences that indicate the currently selected option. Notice how this has also changed the way the title of the document is displayed.

Toolbar
Toolbar

But so far, this does nothing to change the display, so head back to ContentView.swift.

In the HSplitView you have the TextEditor and the WebView. Now there will be three possible combinations:

  • TextEditor alone.
  • TextEditor plus WebView.
  • TextEditor plus something else to display the raw HTML.

To handle the first two options, wrap the WebView in an if like this:

if previewState == .web {
  WebView(html: document.html)
    .frame(minWidth: 200)
}

The body should end up looking like this:

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,
         idealWidth: 600,
         maxWidth: .infinity,
         minHeight: 300,
         idealHeight: 400,
         maxHeight: .infinity)
  .toolbar {
    PreviewToolBarItem(previewState: $previewState)
  }
}

Build and run the app again. The web version of the Markdown text is hidden when you select either of the first two buttons and appears again when you select the third button.

No preview
No preview

Adding the HTML text preview

For the raw HTML display, add this underneath the previous if statement:

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)
  }
}

Here’s what is going on:

  1. After checking to see if this view should be visible, the new view starts with a ScrollView so that the text can be seen even if it’s longer than the height of the window.
  2. The actual html text is shown in a Text view, set to fill all the available space with some padding around the edges and with the same minimum width as the web view.
  3. It seems appropriate to use the selected editor font size for this display too.
  4. New in macOS 12, you can make the text inside Text views selectable.

Build and run the app now and you’ll be able to toggle between the three preview states. Use the Preferences window to change the font size and confirm that the HTML view font size changes too.

HTML preview
HTML preview

Markdown in AttributedStrings

At WWDC 2021, one of the new features announced for SwiftUI was an AttributedString that could be formatted using Markdown. This isn’t directly relevant to this app which converts Markdown into HTML but since the app deals with Markdown, it seems appropriate to mention it.

Convert the raw HTML preview to use an AttributedString temporarily by adding this computed property to ContentView.

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.")
}

What is happening here?

  1. Set up the parsing options. These are optional but the defaults don’t preserve linefeeds or tabs.
  2. Try to parse the document’s Markdown text using these options.
  3. Return the parsed AttributedString or an error message.

To display this, change the Text inside the ScrollView to:

Text(attributedString)

Build and run the app, switch to the raw HTML preview mode and you’ll see something like this. Notice how the formatting modifiers like the font size are still applied.

AttributedString from Markdown
AttributedString from Markdown

This technique could be very useful for formatting text in SwiftUI apps, but for this app, switch the Text back to Text(document.html) and delete the computed property.

Installing the app

On an iOS device, when you build and run the app in Xcode, the app is installed 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 because building and running doesn’t copy the app into your Applications folder but buries it deep within your Library.

To install your app so that you can use it yourself, make sure the app is running. Right-click the app icon in the Dock and select Options ▸ Show in Finder. Now you can drag the app into your Applications folder.

Challenge

Challenge: Add another file extension

When you were setting up the file types, you allowed the app to use either “.md” or “.markdown” for the file extensions. But some people use “.mdown” to indicate 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.

Have a go at implementing this yourself, but check out the challenge folder if you need some help.

Key points

  • Apple provides a starting template for document-based Mac apps that can get you going very 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 have 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, preferences, multiple windows. Now you have the tools to make an app that does all these things.
  • And you have a useful Markdown editor that you can really 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.

Here are some links that might help you with your own apps and file types or with learning how to write in and parse Markdown:

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.
© 2026 Kodeco Inc.