So far in this book, you’ve built two very different Mac apps. First, you made a conventional window-based app using SwiftUI. Next, you created a menu bar app using AppKit.
Now, you’ll learn about another class of app: the document-based app. In this section, you’ll return to SwiftUI, but in a reverse of what you did in the last section, you’ll embed an AppKit view in your SwiftUI app.
The app for this section is 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, and you’ll add an HTML preview.
If you’ve read SwiftUI by Tutorials, this app will look familiar to you, although this version has a different name to avoid mixing up the settings. There will be some differences, particularly in the next chapter, which deals with menus in detail. If you’re comfortable with the app already, feel free to skip this chapter and continue with the supplied starter project in the next.
Setting Up 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, each displaying its own content. Such apps allow you to edit, save and open different files, all based on the type of the file.
Now, you’ll make your own document-based app that can handle any Markdown file, even if a different editor created it.
Start Xcode and create a new project. Select macOS and choose Document App.
Document app template
Make sure that the interface is SwiftUI and the language is Swift. Call the app MarkDowner.
Once you’ve saved the project, build and run the app. Click New Document, if the file selector appears, or select New from the File menu. This gives you 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.
Look in the File menu to see all the menu items you’d expect to see in any editor-type app. Select Save from the File menu or press Command-S.
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 will make it easier to follow the next part of this chapter.
Finder Preferences
The default app uses a file extension of .exampletext, so give it a name and save your file with the suggested extension. Close the window and create a new window using Command-N. Now open your saved document by choosing it from File ▸ Open.
So you already have an app that edits, saves and opens documents. And you haven’t even looked at the code!
Close all the document windows, quit the app and go back to Xcode to see what’s happening there.
The Default Document App
There are three .swift files in your project
MarkDownerApp.swift is similar to the App.swift files you’ve seen in other SwiftUI projects, but instead of the body containing a WindowGroup, it contains a DocumentGroup.
You initialize a DocumentGroup with an instance of the document type, in this case MarkDownerDocument. In the closure, you provide the view that displays the data from this file, passing it a binding to the file’s document, so changes to the document can flow back.
If your app supported more than one document type, you’d add more than one DocumentGroup here.
The view is set to ContentView as usual, but with the document parameter.
ContentView.swift takes in this document and uses its text property to populate a TextEditor. This type of view allows editing long chunks of text.
The real magic happens in MarkDownerDocument.swift. This is where you configure the document type, and this is what saves and opens documents.
Start by looking at 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 soon when you customize the app to handle Markdown files.
In MarkDownerDocument, you have a text property that holds the contents of the document. Its initializer sets the default text you saw in each new window when you ran the app. The readableContentTypes property dictates what document types this app can open, using the UTType defined earlier.
The init and fileWrapper methods handle all the work of opening and saving the document files. Right now, they’re using the .exampletext file extension, but 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 using 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 are able to open that type of file. Finder knows what apps can open that file 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 need three pieces of information about the file:
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. You should always check here first when working out the file types for an app. But in this case, it doesn’t help, as Markdown isn’t there.
However, an internet search for markdown uniform type gets you to John Gruber, the inventor of Markdown. He says the Uniform Type Identifier should be net.daringfireball.markdown, and this conforms to public.plain-text.
Searching for Markdown at FileInfo.com tells you the most popular file extensions for Markdown are .markdown and .md.
This gives you all the data you need to switch your app from working with plain text to working with Markdown text.
Setting a Document Type
Select the project at the top of the Project navigator list. Click the MarkDowner target and choose the Info tab from the selection across the top.
Expand the Document Types section and change Identifier to net.daringfireball.markdown.
Document types
Next, expand the Imported Type Identifiers section and make the following changes:
Description: Markdown Text
Identifier: net.daringfireball.markdown
Extensions: markdown, md
Note: If you use a different extension for your Markdown files, see Challenge 1 below.
All the other settings here can stay the same as the Conforms To field already contains public.plain-text.
Imported types
That’s configured your app; now you have to change MarkDownerDocument to use these new settings.
Go back to MarkDownerDocument.swift and replace the UTType line with this:
UTType(importedAs: "net.daringfireball.markdown")
Next, right-click exampleText, select Refactor ▸ Rename… and rename it to markdownText.
And, so you can tell it’s worked, change the default text in init to # Hello, MarkDowner! which is the Markdown format for a level 1 header.
Testing the File Settings
Build and run the app and create a new document. The default text is now # Hello MarkDowner!. Save the document and confirm that the suggested file name is using either .md or .markdown for the file extension.
Note: Which one it chooses seems random, maybe depending on what you’ve used with other apps in the past.
Save and close your new document and then find the file in Finder. Right-click it to show its Open With menu.
Open With Markdowner.app
You see MarkDowner listed there because your settings told Finder that your app opens Markdown files. If you have any Markdown files created by another app, right-click on one of them and open it in MarkDowner.
Your app doesn’t do anything with the Markdown text yet, but it can now edit, save and open any Markdown files. Great work! Now to learn more about Markdown.
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:
# Important Header
## Less Important Header
[Ray Wenderlich](https://www.raywenderlich.com)
- List Item 1
- List Item 2
- List Item 3
I’m sure you’ll agree that the Markdown version is easier to write, easier to read and more likely to be accurate.
You can find out more about Markdown from this very helpful cheat sheet.
In MarkDowner, you write text using Markdown. The app will convert it to HTML and display it to the side in a web view.
Swift has the ability to convert certain Markdown elements into an AttributedString. This can be really useful for formatting parts of your UI. It’s not what you want here, because it doesn’t create HTML. But there are several Swift Packages that can. The one you’re going to use in this app is Swift MarkdownKit.
Converting Markdown to HTML
If you worked through the previous section, or if you’ve used the Swift Package Manager in an iOS app, then you’ll be familiar with this process.
In Xcode, select the project in the Project navigator and this time, click the MarkDowner project instead of the target. Go to the Package Dependencies tab and click the plus button to add a new dependency.
Add Package Dependency
Copy this URL into the search field at the top right to search for the package.
https://github.com/objecthub/swift-markdownkit
When Xcode has found the package, make sure it’s selected and click Add Package to download it.
Finding the MarkdownKit 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 add it into your project.
Adding the MarkdownKit package.
The next step is to edit MarkDownerDocument.swift so it can create an HTML version of the document. To use the package you just added, you need to import it. Add this to the other imports at the top of the file:
import MarkdownKit
In MarkDownerDocument, 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 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 Markdown text, and this is what each document file saves. The other is the HTML version of this text that’s derived from the text using the MarkdownKit package.
Embedding an AppKit View
Now that you’ve set up MarkDownerDocument with an html property, you need a way to display it. The obvious way to display HTML is inside some sort of web view. The problem is that SwiftUI doesn’t have a web view — at least, not yet. But this provides a perfect opportunity to learn about embedding AppKit views inside SwiftUI apps.
If you’ve done this in an iOS app, you’ll have used UIViewRespresentable to embed a UIKit view. For embedding an AppKit view, you use NSViewRepresentable, but it works in exactly the same way, if you replace every UI with NS.
Create a new Swift file called WebView.swift and replace its contents with this code:
You need the SwiftUI library to use NSViewRepresentable, and the WKWebView you’ll embed is in the WebKit framework.
This structure defines a SwiftUI view named WebView. It conforms to NSViewRepresentable.
The structure has a single property to hold the HTML text.
NSViewRepresentable has two required methods: makeNSView(context:) creates and returns the NSView, in this case a WKWebView.
The second required method is updateNSView(_:context:). Whenever there is a change to the properties that requires a view update, the system calls this method. In this case, every time the HTML changes, the web view reloads the HTML in the WKWebView.
Now, you have access to a new SwiftUI view called WebView that contains a WKWebView.
Displaying the HTML
Open ContentView.swift and replace the contents of body with this:
You want to display the Markdown and the HTML side-by-side, in resizable panes. SwiftUI for macOS has a view designed specifically for this, called HSplitView. There’s a VSplitView too, if you want to stack the views vertically, but a horizontal split is better for this app.
Inside the HSplitView, TextEditor is exactly as it was before. The new part is the WebView you just created. You’re passing the HTML version of the document’s text to this view.
Don’t build and run yet. It may look like everything is set up, but it won’t work.
The Mac Sandbox Again
In Section 1, you found you had to open Outgoing Connections (Client) to allow downloads from the internet. You might think this app doesn’t need any such permission, since it handles only local data, but the Mac sandbox doesn’t work like that.
To load anything into a WKWebView, even a local HTML string, you need to open the sandbox in exactly the same way.
Click the project at the top of the Project navigator and select the MarkDowner target, then choose the Signing & Capabilities tab.
Check Outgoing Connections (Client) to allow your WebView to load the HTML.
Sandbox setting
Now, build and run. Test some Markdown. Copy the sample from above if you want a starter. Try resizing the window and dragging the divider to resize each pane.
Resizing the window.
You can make each subview tiny, or even make it disappear. This is not ideal, so you need to fix that.
Limiting the Frames
As you discovered in Chapter 2, “Working With Windows”, it’s important to set frames for your window to limit its size.
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, replace the contents of body with this:
The additions are all frame modifiers, but here is what they’re doing:
Inside HSplitView, set the minimum width of TextEditor to 200.
Apply the same width limit to WebView.
Give HSplitView a more complete frame that sets its minimum, ideal and maximum sizes. The minimum width is enough to fit both subviews at their minimum widths. The maximums are infinity so the window can get as large as the user wants.
Build and run again and try resizing each pane and the window. That works better. :]
Adding a Toolbar
Right now, the app allows you to edit Markdown text and render the equivalent HTML in a web view. But it’s sometimes useful to see the actual HTML code generated. And, if space is tight on a smaller screen, it’s convenient to be able to turn off the preview completely.
So now, you’re going to add a toolbar. In the toolbar, you’ll have controls to switch between three possible preview modes: web, HTML code and off.
Start by defining an enumeration for the preview modes. Add this to the end of ContentView.swift, outside any structure:
enum PreviewState {
case web
case code
case off
}
Next, add this property to ContentView:
@State private var previewState = PreviewState.web
This defines a @State property to hold the selected state and sets it to web by default.
Finally, add this to HSplitView after the frame modifier:
// 1
.toolbar {
// 2
ToolbarItem {
// 3
Picker("", selection: $previewState) {
// 4
Image(systemName: "network")
.tag(PreviewState.web)
Image(systemName: "chevron.left.forwardslash.chevron.right")
.tag(PreviewState.code)
Image(systemName: "nosign")
.tag(PreviewState.off)
}
// 5
.pickerStyle(.segmented)
// 6
.help("Hide preview, show HTML or web view")
}
}
What does all this do?
Apply a toolbar modifier to HSplitView.
Insert a ToolbarItem into the toolbar.
The ToolbarItem contains a Picker with its selection bound to the previewState property.
Show an image from Apple’s SF Symbols font for each PreviewState and set the tag to the corresponding case.
Set the picker to use the segmented style.
Apply accessibility text and a tooltip using the help modifier.
When you made a toolbar in section 1, you put the toolbar code in its own file. This is a good idea if your view is complex or the toolbar contains a lot of buttons. In this case, applying it directly still leaves ContentView.swift quite short and readable.
Build and run the app to see a toolbar with these three options at the far right. Click each one to see the visual differences that indicate the currently selected option:
Picker in toolbar
Configuring the Preview
You’ve got the controls to dictate the preview, but your app isn’t responding to them. Right now, in the HSplitView you have the TextEditor and the WebView. But when you allow for the preview options, there are three possible combinations:
TextEditor alone.
TextEditor plus WebView.
TextEditor plus something new to display the raw HTML.
First, to let the user turn off the WebView, Command-clickWebView inside HSplitView and choose Make Conditional.
Note: If your Xcode preference sets Command-click to Jumps to Definition, use Command-Control-click to show the menu.
Replace the true placeholder with:
previewState == .web
Build and run the app. Click the three options in the toolbar. The WebView is only visible when you select the web button in the picker, and it disappears when you click either of the others:
Hiding the web view.
Making the WebView appear conditionally added an EmptyView for when it should not appear. But this is where you want to check for previewState being set to code.
Show a ScrollView so you can see all the text, even if there is more than fits in the window.
Display the HTML version of the document text inside a Text view.
Set the frame and padding for the Text view so it has the same minimum width as the others, but expands to fill the ScrollView.
Make the text selectable, so you can copy it.
Build and run now and try out each of the three preview states.
Showing the HTML code.
Challenges
Challenge 1: Add a File Extension
When you were setting up the file types, you allowed the app to use either .markdown or .md for the file extensions. But some people use .mdown for Markdown files. Edit the project so that this is a valid extension. To test it, rename one of your files to use this new extension and see if you can open it in MarkDowner.
Challenge 2: Apply an App Icon
Open the assets folder for this chapter in the downloaded materials, and you’ll find an image file called markdown.png. Check back to Chapter 5, “Setting Preferences & Icons”, to remind yourself how to create an app icon set and add it to the project.
Have a go at implementing these yourself, but check out the challenge folder if you need some help.
Key Points
Apple provides a starting template for document-based Mac apps. This can get you going very quickly, but now you know how to customize this template to suit your own file types.
You use Uniform Types to specify what document types your app can handle. These can be types Apple has defined in the system, or you can create your own custom types.
SwiftUI and AppKit work well together. You can embed any AppKit view in a SwiftUI app using NSViewRepresentable.
Where to Go From Here?
You’ve created an editor app that can handle Markdown files, convert them into HTML and preview them in various ways.
In the next chapter, you’ll add menu commands to this app. They’ll let you style the HTML, adjust font sizes, show some Markdown help and add Markdown snippets to your document.