20.
Building a Mac App
Written by Sarah Reichelt
If you have worked through the previous chapters, you will have 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 are going to write a purely Mac app. The app you will create is going to be the 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 are 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 will create a document-based app from the Xcode template and see how much functionality that provides for free. Then you will 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 up Xcode and create a new project. Select macOS and choose Document App. Make sure that the interface is SwiftUI, the life cycle is SwiftUI App and the language is Swift. Call the app MacMarkDown.
Once you have saved the project, build and run the app.
The app will open with 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 do not 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.
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 take a look at MacMarkDownApp.swift. Instead of the app body containing a WindowGroup, 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 will see that the only view inside the body is a TextEditor. This view is new in macOS 11 and iOS 14 and 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 will 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 on any document file and look at the Open With menu, you will 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 will need three pieces of data:
-
The Uniform File 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://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html which can often be useful when working out file types for an app, but in this case it doesn’t help as Markdown is not 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 data, you are 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”.
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”.
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.
Close the window after saving and then find the file in Finder and right-click on it to show its Open With menu. You will 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 will 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 will 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 will 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 does not have a built-in Markdown converter, so the first thing is to import a Swift Package to do this. The one you are going to use in this app is https://github.com/objecthub/swift-markdownkit.
Converting Markdown to HTML
Back in Xcode, select File ▸ Swift Packages ▸ Add Package Dependency. Enter this URL: https://github.com/objecthub/swift-markdownkit and click Next. Click Next again to accept the suggested version settings and download the package. Then click Finish and the package will be imported into your project.
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 getter for a property that uses MarkdownParser to parse the text and then uses 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 does not 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
final class WebView: NSViewRepresentable {
// 3
var html: String
init(html: String) {
self.html = html
}
// 4
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
return webView
}
// 5
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(
html,
baseURL: Bundle.main.resourceURL)
}
}
Stepping through this:
-
The
SwiftUIlibrary is needed to useNSViewRepresentableandWebKitis needed forWKWebView. -
WebViewwill be the name of the SwiftUI view that this class defines. It conforms to theNSViewRepresentableprotocol which provides a bridge between AppKit NSViews and SwiftUI Views. This must be afinalclass which means that it cannot be sub-classed. -
This class only needs one
Stringproperty to store the HTML text. -
NSViewRepresentablehas two required methods:makeNSViewcreates theNSView, in this case aWKWebView, and returns it. -
The second required method is
updateNSViewwhich 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 its 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 are going to embed the TextEditor and a WebView in an HSplitView. This is another relatively recent addition to SwiftUI in macOS 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 does not makes changes, it only displays, so it does not 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 on the Signing & Capabilities tab. In the App Sandbox section, check Outgoing Connections (Client).
Now 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 try 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 views 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 exapanded 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 :]
Adding a settings window
Nearly all Mac apps have a Preferences window, so now you are going to add one to this app. Make a new SwiftUI View file and call it SettingsView.swift. Update the contents in body to the following:
var body: some View {
Text("Settings")
.padding()
}
Change the default “Hello, world” text to say “Settings” and add 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 up MacMarkDownApp.swift. Inside the body after DocumentGroup add these lines:
Settings {
SettingsView()
}
It is 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 is really small - just large enough to hold the text “Settings” but it is there and now you can configure it.
@AppStorage
At WWDC 2020, Apple announced several new property wrappers for SwiftUI. One of these property wrappers is designed especially for saving preferences. If you have worked through the earlier chapters in this book, you will 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 are 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?
Now Apple has created the@AppStorage property wrapper which makes all this so much easier. Under the hood, it is still using UserDefaults but it handles a lot of the details for us.
Choosing a font size
You are 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.
-
@AppStoragesets 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 is neater if you use the same name for the UserDefaults property and the variable, but this is not 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 just the arrows to change the font size. The editor font size will automatically change as you change the setting.
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 is 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 CommmandMenu 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 modifer directly in place in MacMarkDownApp.swift but since menu definitions can get quite extensive, it makes your files 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: CommandGroupPlacement.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?
- Menu content and its
bodymust conform to theCommandsprotocol so that you can use this struct to set the menu commands for your app. - A
CommandGrouphas to be positioned either before, after or in place of existing menu items. Check out the docs forCommandGroupPlacementto see which menu items have identifiers that you can use. - Here you are 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 is calling a function that will open a URL in the default browser.
- 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 take a 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 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 = .github
This creates a new @AppStorage property for a StyleSheet and sets it to use GitHub style as the default.
Replace the // more menu items will go here comment with this:
// 1
CommandMenu("Stylesheet") {
// 2
Button("GitHub") {
styleSheet = .github
}.keyboardShortcut("1", modifiers: .command)
Button("Lopash") {
styleSheet = .lopash
}.keyboardShortcut("2", modifiers: .command)
Button("Solarized Dark") {
styleSheet = .solarizeddark
}.keyboardShortcut("3", modifiers: .command)
Button("Ulysses") {
styleSheet = .ulysses
}.keyboardShortcut("4", modifiers: .command)
}
Here is what this code is doing:
- To create an entirely new menu, use
CommandMenugiving it the title of the new menu. - Each style has a menu item button with text and a keyboard shortcut. These buttons change the
styleSheetproperty.
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 class. 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 the WebView class:
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.
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. Nothing happens! Try editing the Markdown. Now the web preview uses the selected stylesheet.
The issue here is that changing the styleSheet property does not trigger a change in the html property and so the web view does not update. You are going to have to fool the web view into thinking that the text has changed whenever the style is changed.
Go to ContentView.swift and give it the same @AppStorage property declaration inside ContentView. Inside the body, add this modifier to WebView:
.onChange(of: styleSheet) { _ in
document.refreshHtml()
}
And in MacMarkDownDocument.swift, add this method at the bottom of MacMarkDownDocument:
mutating func refreshHtml() {
let tempText = text
text = ""
text = tempText
}
WebView is now watching for any change to styleSheet and whenever it detects a change, it calls the document’s refreshHtml method. This method does a quick swap to convince SwiftUI that the text has changed and then the WebView will redraw.
Build and run the app and this time, when you select a new stylesheet, it will be used immediately.
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 are going to add another UI element that is very common in Mac apps - the toolbar. In the toolbar, you will 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 are 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 are 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.
- So that this struct can be assigned as the content of a Toolbar, it must conform to the
ToolbarContentprotocol. - A binding variable receives the selected preview state from the parent view and passes any changes back to it.
- The body also needs to conform to the
ToolbarContentprotocol. - ToolbarContent views must be either
ToolbarItemorToolbarGroup. As this is only showing a single view, aToolbarItemis the right one to use. - Since this is going to switch between three possibilities, a segmented picker seems to be a good UI choice.
- 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.
- 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 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.
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)
.onChange(of: styleSheet) { _ in
document.refreshHtml()
}
}
The body should end up looking like this:
var body: some View {
HSplitView {
TextEditor(text: $document.text)
.font(.system(size: CGFloat(editorFontSize)))
.frame(minWidth: 200)
if previewState == .web {
WebView(html: document.html)
.frame(minWidth: 200)
.onChange(of: styleSheet) { _ in
document.refreshHtml()
}
}
}
.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 the first button and appears again when you select the third button.
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)))
}
}
Here’s what is going on:
-
After checking to see if this view should be visible, the new view starts with a
ScrollViewso that the text can be seen even if it is longer than the height of the window. -
The actual html text is shown in a
Textview, set to fill all the available space with some padding around the edges and with the same minimum width as the web view. -
And it seems appropriate to use the selected editor font size for this display too.
Build and run the app now and you will 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.
You now have the basics of a great Markdown editor. The completed project is in the final folder for this chapter.
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 is not quite as simple because building and running does not 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, go to the Project navigator and expand the Products section so that you can see your app. Right-click on it, select Show in Finder and drag the app into your Applications folder.
Challenge
Challenge: Add exports, snippets and Touch Bar to your app
There are a lot more features you could add to your app, but here are some suggestions:
- Exports: it would be neat to offer an option to export the Markdown as HTML, perhaps even including the selected stylesheet.
- Markdown snippets: it can be difficult to remember some Markdown codes, especially for things like links and images, so a toolbar menu that offered Markdown snippets would be very useful.
- Some MacBooks come with a Touch Bar which allows apps to offer a custom set of controls. You can use a
touchBarmodifier to add buttons to the Touch Bar. In Xcode, you can select Touch Bar from the Window menu to see a Touch Bar simulator.
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 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 working 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!
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:
- Markdown Cheatsheet: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
- Apple - Uniform Type Identifiers Reference: https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
- Wikipedia - Uniform Type Identifiers: https://en.wikipedia.org/wiki/Uniform_Type_Identifier
- Daring Fireball - Markdown UTI: https://daringfireball.net/linked/2011/08/05/markdown-uti
- FileInfo.com - search for file extensions: https://fileinfo.com
- MarkdownKit package: https://github.com/objecthub/swift-markdownkit