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:
- Introduction to Mac Development: Create a Project
- Introduction to Mac Development: Build the UI
- Introduction to Mac Development: Connecting UI Elements
- Make a Simple Mac App in Swift
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 ofNSDocument
.
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
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.