Create a Document-Based Mac App in Swift

The Mac development series continues with an article on making document-based apps. You’re going to learn about document-based apps by making a plain text editor.

If you’re new to Mac development, read the following articles before reading this one:

Those articles go into more detail on things that I gloss over in this article.

Document-Based Apps

A document-based app lets people create documents, where each document is stored in its own file. On the Mac document-based apps take advantage of the File menu. The New menu item creates a new document. The Open menu item opens a document. The Save menu item saves the document. Examples of document-based apps are text editors, spreadsheets, and drawing apps.

Apple provides the NSDocument class for Mac apps to work with documents. When you create a document-based app project in Xcode, Xcode provide a subclass of NSDocument for you to work with.

Create the Project

Start by creating a new project in Xcode. Choose File > New > Project to open the New Project Assistant. Choose Cocoa App from the list of Mac app project templates. Click the Next button.

Enter the project name in the Product Name text field. Select the Use Storyboards and Create Document-Based Application checkboxes. Deselect the Use Core Data and Use SwiftUI (Xcode 11 and later) checkboxes. Enter txt in the Document Extension text field because a plain text editor saves plain text files.

Click the Next button. Choose a location to save the project. Click the Create button to finish creating the project.

Project Files

There are three files you will work on in the project.

  • Main.storyboard contains the user interface.
  • ViewController.swift contains the code for the view controller.
  • Document.swift contains the code for the document. The document is a subclass of NSDocument.

The names ViewController and Document are generic. I kept these names for this project because there’s only one view controller in the project and a text editor deals with documents. If you write your own document-based applications, you would benefit from renaming the classes and files ViewController and Document to something more descriptive.

Dealing with the App Sandbox

When you create a new Cocoa project in Xcode, the App Sandbox is turned on. The initial permissions for dealing with user selected files in the sandbox is read-only. This means that you will be unable to open a Save panel to save the document.

There are two ways to fix the issue. First, you can turn off the App Sandbox. Second, you can set the permission and access for user selected files to Read/Write

AppSandboxFileAccess

You can access the App Sandbox settings by selecting the project from the project navigator, selecting the app target from the project editor, and clicking the Capabilities button at the top of the project editor.

Build the User Interface

The user interface for this project is simple. It consists of a text view that covers the whole window.

Open the storyboard. Delete the label in the window that says Your document contents here.

Open the Object Library and type text view in the search field. Drag the Plain Document Content Text View item from the Object Library to the view controller on the storyboard canvas. If you are running an older version of Xcode that doesn’t have a plain document content text view, drag a text view to the view controller.

Resize the text view so it fills the whole view. Select the text view’s enclosing scroll view and open the size inspector. Click the two arrows in the inner autoresizing square. Clicking the arrows will ensure the text view fills the whole window when the window resizes.

If your version of Xcode doesn’t have a plain document content text view, select the text view and open the attributes inspector. Deselect the Allows Rich Text checkbox.

Create an Outlet for the Text View

The next step is to create an outlet for the text view so you can access it in your code. Open the assistant editor and make sure both the storyboard and view controller source code file are open. Select the text view in the storyboard and control-drag inside the ViewController class to create an outlet for the text view.

Add Properties to the Document Class

Now it’s time to start writing some code by adding some properties to the Document class. There are two properties to add. The first property stores the contents of the document. You’re going to need this property when loading the document.

var contents = ""

The second property is a computed property to access the view controller. When saving the document, you’re going to set the contents property to the text view’s contents. To access the text view, the document needs access to the view controller.

var viewController: ViewController? {
    return windowControllers.first?.contentViewController 
        as? ViewController
}

The code takes advantage of the fact the document has only one window controller. Access the first item in the windowControllers array and get its content view controller.

Saving the Document

The text view handles common text editing tasks without you having to write any code. For this project the only code you have to write is the code to save and load documents.

To save the document you must write the dataOfType function. Xcode provides a shell of the function for you to fill in, which you can find in the Document.swift file.

Set the document’s contents property to the text view’s string. Convert the document contents to a Data object by using the String struct’s data function.

override func data(ofType typeName: String) throws -> Data {
    // Save the text view contents to disk
    if let textView = viewController?.textView {
        contents = textView.string
        return contents.data(using: .utf8) ?? Data()
    }
    throw NSError(domain: NSOSStatusErrorDomain, 
        code: unimpErr, userInfo: nil)
}

Notice that you don’t have to write any code to open a Save panel. Cocoa’s document architecture handles opening the Save and Open panels for you.

Loading the Document

To load the document you must write the readFromData function. Xcode provides a shell of the function for you to fill in. The shell has a throw statement in the function. Remove that line of code. If you keep that line, an alert will open when you try to open a document saying that the document can’t be opened.

Loading involves converting a Data object to a string and setting the document’s contents property to that string. The String struct has an initializer that takes a Data object as an argument.

override func read(from data: Data, 
    ofType typeName: String) throws {

    if let fileContents = String(data: data, 
        encoding: .utf8) {

        contents = fileContents
    }
}

Filling the Text View

After loading the document from disk, fill the text view with the document’s contents. Your first instinct may be to fill the text view from the readFromData function. If you do that, you’ll find the view controller is nil, and you won’t be able to access the text view.

What you have to do is override the function viewDidAppear in the view controller. Gain access to the document through the view controller’s window controller and set the text view’s string to the document’s contents.

override func viewDidAppear() {
    // Fill the text view with the document's contents.
    if let document = self.view.window?.
        windowController?.document as? Document {

        textView.string = document.contents
    }
}

Conclusion

If you made it this far, congratulations. You wrote a usable text editor. Adding a text view provides most basic text editing functions so you don’t have to reinvent common behavior. Cocoa’s document architecture handles opening Save and Open panels, reducing the amount of code you have to write. There’s fewer than 20 lines of code to write in this project.

The project is on GitHub for you to download if you have trouble building or running the project.