Home iOS & Swift Books SwiftUI by Tutorials

20
Building a Mac App Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

Savedoc
Savedoc

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.

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.

Doc setup
Rip gimat

Doc setup 2
Joy zoluc 1

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

Testing the new settings

Build and run the app. If there 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.

Save
Bami

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>
# Important Header
## Less Important Header

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

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

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.

Import
Adkogr
Target
Wivlem

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

Adding the HTML preview

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

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

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.

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

Preview
Ztaduik

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.

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

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()
}
Settings {
  SettingsView()
}
Settings
Xobkustq

@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.

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
Stepper(value: $editorFontSize, in: 10 ... 30) {
  Text("Font size: \(editorFontSize)")
}
.frame(width: 260, height: 80)
.font(.system(size: CGFloat(editorFontSize)))
Font-size
Narn-vesu

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.

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)
  }
}
.commands {
  MenuCommands()
}
Help menu
Cekm guhe

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?

@AppStorage("styleSheet") var styleSheet: StyleSheet = .github
// 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)
}

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.

var formattedHtml: String {
  return """
      <html>
      <head>
         <link href="\(styleSheet).css" rel="stylesheet">
      </head>
      <body>
         \(html)
      </body>
      </html>
      """
}
func updateNSView(_ nsView: WKWebView, context: Context) {
  nsView.loadHTMLString(
    formattedHtml, //CHANGE THIS
    baseURL: Bundle.main.resourceURL)
}
.onChange(of: styleSheet) { _ in
  document.refreshHtml()
}
mutating func refreshHtml() {
  let tempText = text
  text = ""
  text = tempText
}
Stylesheet
Nxhpufnous

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.

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

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

Using the toolbar

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

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

if previewState == .web {
  WebView(html: document.html)
    .frame(minWidth: 200)
    .onChange(of: styleSheet) { _ in
      document.refreshHtml()
    }
}
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)
  }
}
No preview
Cu jtinauc

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)))
  }
}
HTML Preview
YYQX Nbebeex

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.

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:

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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.