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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

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:

  1. Create a cache URL and a new empty MarkupDocument at that location.
  2. Save the document to that cache URL location.
  3. If the save fails, you call the import handler with ImportMode.none to cancel the request.
  4. 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:

  1. As long as you are not editing and there is a current document, present MarkupViewController modally.
  2. Dismiss the current MarkupViewController and clean up.
  3. When the document finishes editing you update the document then dismiss the MarkupViewController.
  4. 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:

  1. Return if the document is already being edited.
  2. Open the new document and then open a MarkupViewController.
  3. 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.

completed uidocumentbrowserviewcontroller system