Creating Document-Based Apps with SwiftUI
Xcode 12 Note from the Author
Xcode 12 made a bunch of changes to SwiftUI for document-based apps. If you choose a multi-platform document app project, you will get the text editor project I create in this article without having to write any code. I’m keeping this article up for people who are still using Xcode 11.
The article Make a Markdown Editor in SwiftUI shows how to create a document-based SwiftUI app with Xcode 12.
Start of Original Article
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.
- Enter
PlainText
in the Name text field. - Enter
public.plain-text
in the Types text field.public.plain-text
is the UTI (Uniform Type Identifier) for plain text files. - In the Additional document type properties section, set the CFBundleTypeRole value to
Editor
so people can edit documents. - 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.