Document-Based Apps Tutorial: Getting Started
In this document-based app tutorial, you will explore how you can save and open custom documents and interact with the Files app. By Warren Burton.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Document-Based Apps Tutorial: Getting Started
35 mins
- Introduction
- Getting Started
- Archiving and De-archiving Data
- Saving and Loading Your Composition
- Working With the Document Browser
- Creating a UIDocument Subclass
- Encoding the Document
- Decoding the Document
- Installing UIDocumentBrowserViewController
- Presenting a Container View Controller
- Configuring UIDocumentBrowserViewController
- Configuring Info.plist
- Responding to UIDocumentBrowserViewController Delegate Actions
- Creating Documents
- Importing Documents
- Opening Documents
- Transitioning to the Markup Editor
- Opening a MarkupDocument From a URL
- Supplying DocumentBrowserDelegate With a Presentation Closure
- Allowing Other Apps to Open Documents
- Setting Up the App Delegate
- Updating DocumentBrowserViewController
- Updating RootViewController
- Updating AppDelegate
- Providing a Custom Document Icon
- Adding a ThumbnailProvider Extension Target
- Configuring a QLThumbnailProvider Subclass
- Configuring the Info.plist
- Linking the Framework
- Where to Go From Here?
Creating Documents
The first thing you need to do to create a document is to create a template document in a temporary directory. The app cache directory is a good directory to use.
Add this extension to the end of DocumentBrowserDelegate.swift:
extension DocumentBrowserDelegate {
static let newDocNumberKey = "newDocNumber"
private func getDocumentName() -> String {
let newDocNumber = UserDefaults.standard.integer(forKey: DocumentBrowserDelegate.newDocNumberKey)
return "Untitled \(newDocNumber)"
}
private func incrementNameCount() {
let newDocNumber = UserDefaults.standard.integer(forKey: DocumentBrowserDelegate.newDocNumberKey) + 1
UserDefaults.standard.set(newDocNumber, forKey: DocumentBrowserDelegate.newDocNumberKey)
}
func createNewDocumentURL() -> URL {
let docspath = UIApplication.cacheDirectory() //from starter project
let newName = getDocumentName()
let stuburl = docspath
.appendingPathComponent(newName)
.appendingPathExtension(MarkupDocument.filenameExtension)
incrementNameCount()
return stuburl
}
}
This extension composes a URL in the app cache directory with a sequential name “Untitled 0, 1, …”. The current value of the trailing number is stored in UserDefaults
.
Now, add the following code in the body of documentBrowser(_:didRequestDocumentCreationWithHandler:)
:
// 1
let cacheurl = createNewDocumentURL()
let newdoc = MarkupDocument(fileURL: cacheurl)
// 2
newdoc.save(to: cacheurl, for: .forCreating) { saveSuccess in
// 3
guard saveSuccess else {
importHandler(nil, .none)
return
}
// 4
newdoc.close { closeSuccess in
guard closeSuccess else {
importHandler(nil, .none)
return
}
importHandler(cacheurl, .move)
}
}
In this code, you do the following:
- Create a cache URL and a new empty
MarkupDocument
at that location. - Save the document to that cache URL location.
- If the save fails, you call the import handler with
ImportMode.none
to cancel the request. - Close the document. Assuming that action succeeds, call the import handler with
ImportMode.move
and the cache URL you generated.
This method can be used to hook into a UI for setting up the new document (e.g., a template chooser) but, in all cases, the last action you must take is to call the importHandler
closure, to let the system know you’ve finished.
Importing Documents
Once the import handler is called, the delegate will receive documentBrowser(_:didImportDocumentAt:toDestinationURL:)
or documentBrowser(_:failedToImportDocumentAt:error:)
in the failure case. You’ll set these up now.
Add this property to the top of DocumentBrowserDelegate
:
var presentationHandler: ((URL?, Error?) -> Void)?
This is a closure that you’ll call to present the final URL.
Next, add this line to the body of documentBrowser(_:didImportDocumentAt:toDestinationURL:)
:
presentationHandler?(destinationURL, nil)
Here, you call the closure with the URL of the document.
Now, add this line to the body of documentBrowser(_:failedToImportDocumentAt:error:)
:
presentationHandler?(documentURL, error)
Here, you call the closure with the error that occurred.
Lastly, add this code to the body of documentBrowser(_:didPickDocumentURLs:)
:
guard let pickedurl = documentURLs.first else {
return
}
presentationHandler?(pickedurl, nil)
You have now responded to the open and have created events called by UIDocumentBrowserViewController
.
Build the project to check that everything is working and you can move on to opening the document.
Opening Documents
You have finished implementing DocumentBrowserDelegate
. Open DocumentBrowserViewController.swift again.
First, add these properties to DocumentBrowserViewController
:
var currentDocument: MarkupDocument?
var editingDocument = false
These properties track the active document and editing state.
Transitioning to the Markup Editor
Add this extension to DocumentBrowserViewController.swift:
extension DocumentBrowserViewController: MarkupViewControllerDelegate {
// 1
func displayMarkupController() {
guard !editingDocument, let document = currentDocument else {
return
}
editingDocument = true
let controller = MarkupViewController.freshController(markup: document.markup, delegate: self)
present(controller, animated: true)
}
// 2
func closeMarkupController(completion: (() -> Void)? = nil) {
let compositeClosure = {
self.closeCurrentDocument()
self.editingDocument = false
completion?()
}
if editingDocument {
dismiss(animated: true) {
compositeClosure()
}
} else {
compositeClosure()
}
}
private func closeCurrentDocument() {
currentDocument?.close()
currentDocument = nil
}
// 3
func markupEditorDidFinishEditing(_ controller: MarkupViewController, markup: MarkupDescription) {
currentDocument?.markup = markup
closeMarkupController()
}
// 4
func markupEditorDidUpdateContent(_ controller: MarkupViewController, markup: MarkupDescription) {
currentDocument?.markup = markup
}
}
In this extension, you provide methods to display and dismiss the MarkupViewController
as well as the delegate methods for MarkupViewControllerDelegate
:
- As long as you are not editing and there is a current document, present
MarkupViewController
modally. - Dismiss the current
MarkupViewController
and clean up. - When the document finishes editing you update the document then dismiss the
MarkupViewController
. - When the content updates you update the document.
Opening a MarkupDocument From a URL
Next, add this extension to DocumentBrowserViewController.swift:
extension DocumentBrowserViewController {
func openDocument(url: URL) {
// 1
guard isDocumentCurrentlyOpen(url: url) == false else {
return
}
closeMarkupController {
// 2
let document = MarkupDocument(fileURL: url)
document.open { openSuccess in
guard openSuccess else {
return
}
self.currentDocument = document
self.displayMarkupController()
}
}
}
// 3
private func isDocumentCurrentlyOpen(url: URL) -> Bool {
if let document = currentDocument {
if document.fileURL == url && document.documentState != .closed {
return true
}
}
return false
}
}
Here, you provide logic to open the document:
- Return if the document is already being edited.
- Open the new document and then open a
MarkupViewController
. - Check if the document is already open by making a couple of logic checks. This is in a separate method to make the flow of the main method more obvious.
Supplying DocumentBrowserDelegate With a Presentation Closure
Next, add this code at the end of the method installDocumentBrowser()
:
browserDelegate.presentationHandler = { [weak self] url, error in
guard error == nil else {
//present error to user e.g UIAlertController
return
}
if let url = url, let self = self {
self.openDocument(url: url)
}
}
In this code block, you give the DocumentBrowserDelegate
instance a closure to use for presenting the document. If there is an error, you handle it “tutorial-style” by ignoring it (in a real app, you’d probably want to show the user a message). Otherwise, follow the path and open the document URL.
You use a weak reference in the closure capture list to avoid a retain cycle between DocumentBrowserViewController
and DocumentBrowserDelegate
.
You’ve now added code to open the document from the URL. You can also bring the MarkupViewController
back into play.
You’re almost there. Just one small wiring change in MarkupViewController
to be done.
Open MarkupViewController.swift in Markup/Primary Views and find viewDidLoad()
.
Delete these two lines:
observeAppBackground()
loadDocument()
and replace with this line:
loadCurrentContent()
You don’t need to observe the app going into the background any more, because UIDocument
does that for you. And you don’t need to load a default document any more, because you now inject the MarkupDescription
instance when you create the controller. You just need to get that content on the screen.
Build and run. Now, you have a fully fledged document UI system. You can create new documents or open existing ones.