Creating Document-Based Apps with SwiftUI

I have not seen any articles or tutorials on creating a document-based app with SwiftUI so I’m writing one. In this tutorial you will build an iOS plain text editor.

If you haven’t already, I recommend reading two articles before going through the tutorial. Creating Document-Based iOS Apps Part 1 provides an overview of creating document-based iOS apps. Using Text Views in a SwiftUI App provides an explanation of using a UIKit text view in a SwiftUI app. SwiftUI does not currently have a built-in text view.

Create the Project

Start by creating a project. Create an iOS document-based app project. Choose SwiftUI from the User Interface menu if it’s not already selected.

The most interesting files Xcode creates for a document-based SwiftUI app are the following:

  • DocumentBrowserViewController.swift contains code for the document browser, where people create and open documents.
  • DocumentView.Swift contains code for the document’s main view.
  • Document.Swift contains code for the document.

Creating a New Document

If you run the project, you’ll notice that tapping the Create Document button does nothing. You must write some code to create the document when someone taps the button. The easiest way to create a new document is to add an empty file to the project. The empty file should have the same file extension as the document’s. This file will be copied to the app bundle when building the project. You’ll have to write some code to load the file from the app bundle.

One last thing to do to create documents properly is to configure the document type in Xcode so that the app is an editor of plain text files.

Add an Empty Document File

Choose File > New > File to add a new file to the project. Select Empty from the list of iOS file templates. The empty file is in the Other section. You have to scroll down a bit to reach the Other section.

Name the file. I named the file New Document.txt. You can choose a different name, but remember the name.

Load the Empty Document

Open the DocumentBrowserViewController.swift file and go to the didRequestDocumentCreationWithHandler function. Replace the following line of code:

let newDocumentURL: URL? = nil

With the following code:

let newDocumentURL: URL? = Bundle.main.url(
    forResource: "New Document", withExtension: "txt")

This code loads the empty document file from the app bundle and uses that as the base for a new document.

There is one more piece of code to change. In the same function, find the following code:

if newDocumentURL != nil {
    importHandler(newDocumentURL, .move)
} else {
    importHandler(nil, .none)
}

There is a problem with the code inside the if block.

importHandler(newDocumentURL, .move)

This line of code moves the file from the application bundle. Your app will crash the second time you create a document because the empty document file was moved out of the application bundle. The fix is to copy the file from the application bundle when creating a new document.

importHandler(newDocumentURL, .copy)

Edit the Document Type

Xcode initially sets the document type for an iOS document-based app to be a viewer of image files. You must configure the document type in Xcode so that the app is an editor of plain text files. You’re making a text editor, not a text viewer.

Select the project from the project navigator to open the project editor. Select the app target from the left side of the project editor. Click the Info button at the top of the project editor to access the document types. Click the disclosure triangle next to Document Types.

DocumentTypes

  1. Enter PlainText in the Name text field.
  2. Enter public.plain-text in the Types text field. public.plain-text is the UTI (Uniform Type Identifier) for plain text files.
  3. In the Additional document type properties section, set the CFBundleTypeRole value to Editor so people can edit documents.
  4. Set the LSHandlerRank value to Alternate to ensure this text editor isn’t the default text editor for all plain text files.

Create the Data Model

The data model for this project is simple. The document is the data model. All you have to do is add a property to store the text. Open the Document.swift file and add the following code inside the Document class:

var text = ""

The code declares a property to store the text and has the initial value of an empty string.

Building the Text View

Now it’s time to add the text view so people can edit text. Currently SwiftUI does not include a text view so you have to write code to create a UIKit text view. But it’s not too much code. Add a new Swift file to your project for the text view. Import the SwiftUI and UIKit frameworks. Add the following struct:

struct TextView: UIViewRepresentable {

}

The text view must conform to the UIViewRepresentable protocol, which is the protocol that allows the use of UIKit views in SwiftUI apps. To conform to the protocol, you must write the following functions:

  • makeUIView
  • updateUIView
  • makeCoordinator

The makeUIView function creates and configures the view. The function takes an argument of type Context and returns the type of view you want to make, which is UITextView for a text view. The Context type is a type alias for the context where updates to the UIKit view take place.

func makeUIView(context: Context) -> UITextView {
    let view = UITextView()
    view.isScrollEnabled = true
    view.isEditable = true
    view.isUserInteractionEnabled = true
    view.contentInset = UIEdgeInsets(top: 5, 
        left: 10, bottom: 5, right: 5)
    view.delegate = context.coordinator
    return view
}

The code listing creates a text view, configures the view so people can edit large amounts of text, and adds some padding so the text isn’t on the left edge of the screen. The code also sets the view’s delegate to the context’s coordinator, which you will create shortly.

The updateUIView function handles updates to the view.

func updateUIView(_ uiView: UITextView, context: Context) {

}

I will fill in this function later.

The makeCoordinator function creates a coordinator so the UIKit view can communicate with data in SwiftUI.

func makeCoordinator() -> TextView.Coordinator {
    Coordinator(self)
}

Create the Coordinator

Add a Coordinator class inside the struct for the text view.

class Coordinator: NSObject, UITextViewDelegate {
    var control: TextView

    init(_ control: TextView) {
        self.control = control
    }
}

The class inherits from NSObject, which is the base class for all UIKit classes. A text view’s coordinator should conform to the UITextViewDelegate protocol so it can respond to text view notifications, such as the text changing.

The coordinator needs a property to hold the UIKit view and an initializer. I will be adding more to the coordinator later in the article.

Add the Text View to the Document View

The last step to adding the text view is to have the document view display it. Open the DocumentView.Swift file. Inside the body property you should see a VStack that contains a HStack and a button. The HStack shows the name of the file. The button lets you go back to the browser when you’re done writing.

The text view should appear between the HStack and button. Add the following line of code between the HStack and button:

TextView()

If you build and run the project, you should be able to create a new document and type in it.

Connect the Text View to the Document

For the app to do more than let people type text in a text view, you must connect the text view to the document so the text you type updates the document as well. In the document view, you should see the following variable:

var document: UIDocument

This variable contains a reference to the document. Make the following changes to the variable:

@State var document: Document

The @State property wrapper will allow the text view to bind to the document in order to display the document’s text. Changing the type of the variable to Document is also necessary for the text view to work with the document properly.

Now add the following property to the text view:

@Binding var document: Document

The @Binding property wrapper binds the document property in the text view to the document property in the document view. The text view and document view are both pointing to the same document. The text view now has access to the document.

Now that you created the binding you must go back to the document view and change the call to create the text view by supplying the document as an argument.

TextView(document: $document)

The $ at the start of $document indicates that you are passing a binding to the text view. You must pass a binding so the document view and text view both point to the same document.

Fill the updateUIView Function

Now that the text view has access to the document, you can write the text view’s updateUIView function. Set the text view’s contents to the document’s contents.

func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = document.text
}

Handling Text Changes

One last thing to do is to update the document when the text view contents change. Add the following function to the Coordinator class:

func textViewDidChange(_ textView: UITextView) {
    control.document.text = textView.text
    control.document.updateChangeCount(.done)
}

The first line of code sets the document’s contents to the text view’s contents. The second line marks the document as changed so it will be saved.

Save the Document

At this point you’re finished with the SwiftUI material, but there’s still some work to do to finish the app. The text editor is missing one really big feature: saving text. Open the Document.Swift file. Xcode created a blank contents function for saving the document.

What you have to do is convert the text in the document to a Data object and archive that object. Apple provides the NSKeyedArchiver class and an archivedData function so you can do both steps in one line of code.

override func contents(forType typeName: String) throws -> Any {
    return try NSKeyedArchiver.archivedData(withRootObject: text,
        requiringSecureCoding: true)
}

The root object is the document’s text. Requiring secure coding ensures your app will be able to unarchive the data when loading the document.

Load the Document

After saving the document, the next task is to load the document when someone chooses a document from the browser. Xcode supplies a blank load function, where you write the code to load the document from disk.

The first argument to the load function is the contents of the file. You must first cast the contents to the Data type. Call the NSKeyedUnarchiver class’s unarchiveTopLevelObjectWithData function to unarchive the text from the file. The last step is to set the document’s text to the contents of the file.

override func load(fromContents contents: Any, 
    ofType typeName: String?) throws {

    guard let data = contents as? Data else { return }
    guard let fileContents = try 
        NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) 
            as? String else { return }
    text = fileContents
}

Conclusion

Now you have a simple, working text editor. You can create documents, edit text, save documents, and open them. You can use this project as a foundation for building your own text editing app, such as a Markdown editor. I have the project on GitHub for you to download if you run into issues.