22.
Building a Mac App
Written by Sarah Reichelt
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:
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.
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 type identifiers, 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 identifier” gets you to Daring Fireball, where John Gruber, the inventor of Markdown, says that the Uniform Type Identifier is “net.daringfireball.markdown” and that this conforms to “public.plain-text”.
Search for “markdown” at FileInfo.com, and you’ll 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. Select the MacMarkDown target and choose the Info tab from the selection across the top.
Expand the Document Types section and change the Identifier to “net.daringfireball.markdown”:
Next, expand the Imported Type Identifiers section and make the following changes:
Description: Markdown Text
Extensions: md, markdown
Identifier: net.daringfireball.markdown
All the other settings can stay the same as the Conforms To field already contains “public.plain-text”.
There’s 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 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:
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 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 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>
To write the same in Markdown, you can use:
# Important Header
## Less Important Header
[Kodeco](https://www.kodeco.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 helpful cheatsheet.
In MacMarkDown, you write text using Markdown. The app will convert it to HTML and display that 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’ll use is 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 + button to add a new dependency. Enter this URL into the search field at the top right:
https://github.com/objecthub/swift-markdownkit
When Xcode has found the package, make sure it’s selected and click Add Package. Xcode starts downloading it for you:
Once the download is complete, you’ll see a new dialog asking you what parts of the package you want to use. Check the MarkdownKit Library and click Add Package to insert it into your project:
The next step is to edit MacMarkDownDocument.swift so it can create an HTML version of the document. To use the new package, add import MarkdownKit at the top of the file:
import MarkdownKit
Under the text property, 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 using MarkdownKit’s MarkdownParser to parse the text and its HtmlGenerator to convert it to HTML.
Your document now has two properties. One is the text that’s saved in each document file. The other is the HTML version of that text that MarkdownKit creates.
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.
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
// 4
func makeNSView(context: Context) -> WKWebView {
WKWebView()
}
// 5
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(
html,
baseURL: Bundle.main.resourceURL)
}
}
Stepping through this:
-
NSViewRepresentableis part of theSwiftUIlibrary, andWKWebViewis in theWebKitlibrary. -
WebViewis the name of your custom SwiftUI view that this structure defines. It conforms to theNSViewRepresentableprotocol, which provides a bridge between AppKit’s views and SwiftUI. - This
structonly needs one property: aStringto store the HTML text. -
NSViewRepresentablehas two required methods:makeNSViewcreates and returns theNSView, in this case aWKWebView. - The second required method is
updateNSView. SwiftUI calls this whenever there’s a change to the properties that requires a view update. In this case, every timehtmlchanges, the web view reloads 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 an 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 that 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’s 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 generally a good idea 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 allows your WebView to load content:
Build and run the app:
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 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.
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 makes sure they can never get narrower than 200:
.frame(minWidth: 200)
And add this frame modifier to the HSplitView:
.frame(minWidth: 400, minHeight: 300)
This sets the minimum size for the window but allows users to expand it as much as they want. The minimum width is big enough for both panes at their minimum widths.
Build and run again and try resizing each pane and the window. That’s better. :]
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()
}
This changes the default “Hello, world” text to say “Settings” and adds a padding() modifier, so when you run the app, you can confirm that the correct view appears.
Now it’s time to configure the app to show this view as the Settings window.
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’s what this does:
- Create a Settings… menu item in the app’s File menu.
- Add the standard keyboard shortcut: Command-,.
- Set up a settings window titled “MacMarkDown Settings”.
- Configure the settings window to display
SettingsView. - Establish window controls so only one copy of this window is ever created. Trying to open Settings again if the window is already open just brings it to the front.
Not bad for what could be a single line of code. :]
Build and run the app, then select Settings… from the File menu or type Command-, and your Settings view appears. It’s tiny — only large enough to hold the text “Settings” — but it’s there, and now you can modify it:
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.
Previously, you may have used UserDefaults, which works well for storing small chunks of user data, 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 app has drawn the display?
The@AppStorage property wrapper makes all this so much easier. Under the hood, it’s still using UserDefaults but it handles all these 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
This is only one line, but it packs in a lot of functionality:
-
@AppStoragesets up this property to use theAppStorageproperty wrapper. -
The text in brackets assigns the name of the
UserDefaultssetting. -
Then define the property as usual, with a type and a default value. It’s neater if you use the same name for the
UserDefaultsname and the property name, but this isn’t strictly necessary.
Now for some UI to change this setting. Replace the default body contents with:
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 allows ContentView to access this setting even if the user has never opened the Settings window.
Add a font modifier to the TextEditor:
.font(.system(size: CGFloat(editorFontSize)))
Now build and run the app again. Make sure there’s some text in the editor, so you can see it change. Open the Settings window and use the arrows to change the font size. The editor font size automatically changes as you change the setting:
Make a new window, so you have more than one open at the same time. Confirm the font changes size in both windows. And if you quit and restart the app, your new font size setting is still there.
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.
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. You apply both of these by adding a commands modifier to the DocumentGroup.
You can include the contents of the commands modifier directly in MacMarkDownApp.swift but since menu definitions can get quite extensive, it makes your project easier to read if you separate them 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 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?
- Menu content and its
bodymust conform to theCommandsprotocol so you can use this structure to set up menus for your app. - You position a
CommandGroupeither before, after or in place of existing menu items. Deletehelpand press Escape to see the autocomplete menu of items you can use. Reset tohelpwhen you’ve finished looking. - Here you’re adding a
Buttonas a menu item with aDividerbelow it to make the menu look better. - The button has a keyboard shortcut of Command-/.
- The menu item button calls a method to open a URL in the default browser.
- If you typed a web address incorrectly, 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:
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?
Open the assets folder in the downloads for this chapter and find the StyleSheets folder. Drag this folder into your Project navigator, checking Copy items if needed, selecting Create groups and confirming that the folder will be 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 = .github
This creates a new @AppStorage property for a StyleSheet and sets it to use the GitHub style as the default.
Replace the // more menu items go here comment with:
// 1
CommandMenu("Stylesheet") {
// 2
ForEach(StyleSheet.allCases, id: \.self) { style in
// 3
Button(style.rawValue) {
styleSheet = style
}
// 4
.keyboardShortcut(style.shortcutKey, modifiers: .command)
}
}
Here’s what this code does:
- To create an entirely new menu, use
CommandMenugiving it the title of the new menu. - Loop through all the cases in the
StyleSheetenum. - Each style has a menu item button with the title set to the
rawValuestring for the case. These buttons change thestyleSheetproperty. - Set a keyboard shortcut 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, you can use a direct link to the CSS files inside the app.
Replace updateNSView(_:context) with.
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(
formattedHtml, // Changed line
baseURL: Bundle.main.resourceURL)
}
Build and run the app. Use your new menu to change to a different 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. 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’s very common in Mac apps — the toolbar. In the toolbar, you’ll add controls to switch between three preview modes: web, HTML and off.
You add a toolbar as a modifier to a view, in this case ContentView. It can be 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.
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
}
Next, add this structure:
// 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")
}
}
}
This looks like a lot, but take it one step at a time.
- So that you can set this structure as the content of a Toolbar, mark it as conforming to the
ToolbarContentprotocol. - A binding variable receives the selected preview state from the parent view and passes any changes back to it.
- The
bodyalso needs to conform to theToolbarContentprotocol. -
ToolbarContentviews are eitherToolbarItemorToolbarItemGroup. As this only shows a single view, aToolbarItemis the right one to use. - Since this switches between three possibilities, a segmented picker is a good UI choice.
- Each segment displays an SF Symbol image and has a tag set to the corresponding
PreviewStatecase. Apple’s SF Symbols app or Xcode’s Library shows all these icons, so you can search for appropriate ones. - The
helpmodifier provides a tooltip and accessibility text.
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
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 property, so that changes flow 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 changes the look of the document’s title:
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 are three possible combinations:
-
TextEditoralone. -
TextEditorplusWebView. -
TextEditorplus 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 ends 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, minHeight: 300)
.toolbar {
PreviewToolBarItem(previewState: $previewState)
}
}
Build and run the app again. The web version of the Markdown text disappears when you select either of the first two buttons and appears again when you select the third button:
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)
}
}
Here’s what’s going on:
- After checking to see if this view should be visible, the new view starts with a
ScrollView, so that the text can scroll if it’s longer than the height of the window. - Use a
Textview to show the HTML text. Set the view to fill all the available space with some padding around the edges and with the same minimum width as the web view. - It seems appropriate to use the selected editor font size for this display too.
- Make the text inside
Textviews selectable, so users can copy it.
Build and run the app now and you’ll be able to toggle between the three preview states. Use the Settings window to change the font size and confirm that the HTML view font size changes too:
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.
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’s happening here?
- Set up the parsing options. These are optional but the defaults don’t preserve linefeeds or tabs.
- Try to parse the document’s Markdown text using these options.
- Return the parsed
AttributedStringor 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 font modifier is still applied, but not all the Markdown tags are supported.
This technique could be useful for formatting display text in SwiftUI apps, but for this app, switch the Text back to Text(document.html) and delete the computed property.
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.
To install your app so you can use it easily, make sure the app is running. Right-click the app icon in the Dock and select Options ▸ Show in Finder. Now you can drag MacMarkDown.app into your Applications folder.
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.
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 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.