Advanced iOS Summer Bundle

3 brand-new books on SwiftUI, Combine and Catalyst — $99.99 for a limited time!

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.

4.5/5 2 Ratings

Version

  • Swift 4.2, iOS 12, Xcode 10
Note: This tutorial requires at least Xcode 10, Swift 4.2, and iOS 12.

Introduction

It used to be the case that, if your app used documents, you needed to create your own document browser UI and logic. This was a lot of work. With iOS 11, that all changed. It’s no longer impossible to share documents from your app’s own sandbox with other apps on the same device. iOS 11 introduced both the Files app and a new public API called UIDocumentBrowserViewController that provides most of the functions that document-based apps use.

Document API ecosystem

UIDocumentBrowserViewController provides developers with several features:

  • A system UI that all users will be able to recognize and use.
  • No need to write your own UI and associated logic to deal with file management locally or on iCloud.
  • Simple sharing of documents globally across the user’s account.
  • Fewer bugs because you are writing less code.

And via UIDocument:

  • File locking and unlocking.
  • Conflict resolution.

In this tutorial, you will cover creating a simple UIDocument subclass implementation, using UIDocumentBrowserViewController in your document-based app. You will also use a Thumbnail Provider extension to create custom icons for your documents.

To do this tutorial, you will need:

Getting Started

The starter app, called Markup, can be found using the Download Materials link at the top or the bottom of this tutorial. The app is a simple tool that allows you to add text over the top of an image. It uses a Model-View-Controller pattern to decouple the data from the UI.

Open the Markup.xcodeproj file in the Markup-Starter folder. Select the Markup project in the Project navigator. You will see that there are two targets. The app Markup and a framework target MarkupFramework:

You’re using a framework here because later on you’ll be adding an app extension. The framework allows you to share code between the app and the extension.

Starter project structure

You don’t need to have an in-depth understanding of this app’s workings in order to do this tutorial; it’s bolted together with stock UIKit parts and modeling glue. Since there’s a lot of material to cover, the starter app contains a lot of stub files to help you get going — even if you don’t fully understand everything that’s there, you’ll still be learning a lot about the topic. Feel free to poke around the code later to see how it works.

Next, ensure that Markup is selected in the target selector. Choose the iPad Pro (10.5-inch) simulator:

select correct target

The app is universal and will work on any device if you want to try it later.

Build and run. You will see the following UI:

Choose any available image and add some random words to the title and description fields. They should render in the bottom half of the screen. You can export a JPEG image using the share button on the right of the screen above the rendering:

Populated UI

Archiving and De-archiving Data

Go to the Project navigator and open the folder Markup Framework/Model. Inside you will find two files:

  • MarkupDescription.swift provides a protocol for the data structure that describes the page: title, long description, image, color and rendering style.
  • ContentDescription.swift is a class that adopts the MarkupDescription protocol. It provides a concrete implementation that can be instantiated.

ContentDescription conforms to NSCoding. This means that you can use an NSKeyedArchiver to turn an instance into data, or you can use an NSKeyedUnarchiver to recover an instance from data. Why this is useful will become clear later in the tutorial.

Serialization Cycle

In this app, you use NSCoding instead of Codable because UIColor and UIImage don’t conform to Codable. The important thing, here, is that you can encode and decode Data.

Note: If you’re unfamiliar with serialization, you can learn more about the topic in these tutorials here and here.

Saving and Loading Your Composition

Build and run. Next, create something with an image, title and description.

Put the app into the background with the Hardware > Home menu item (or Command-Shift-H). You should see a message like this in the Xcode console (the path will be a little different, that’s fine):

save OK to file:///Users/yourname/.../Documents/Default.rwmarkup

If you want to see the code behind this, have a look at observeAppBackground() in MarkupViewController.

Stop the app. Build and run again. Your previous composition should appear in front of you, ready for editing.

Working With the Document Browser

At this stage, a user can save and edit exactly one file. If you want an App Store success, you’re going to need to do better.

In the section that follows, you’ll install and use a UIDocumentBrowserViewController to allow your customers the ability to work with any number of documents.

Creating a UIDocument Subclass

UIDocumentBrowserViewController works together with instances of UIDocument. UIDocument is what’s known as an abstract base class. This means that it can’t be instantiated by itself; you must subclass it and implement some functionality.

In this section, you’ll create that subclass and add the needed functionality.

Open the Markup/UIDocument Mechanics folder in the Project navigator. Open MarkupDocument.swift.

DocumentError defines some Error types for potential failure events. MarkupDocument is a subclass of UIDocument that contains stubs for the two methods that must be implemented in a valid UIDocument.

When you save or close the document, the UIDocument internals will call contents(forType:) to get the data that represents your document in order to save the data to the file system. When you open a document, UIDocument will call load(fromContents:ofType:) to supply you with the encoded data in the content parameter.

The contents passed into the method can be one of two things:

  1. Data for when your data is a binary blob. You’ll be using this format in this tutorial.
  2. A FileWrapper for when your document is a package. Packaged — a.k.a. bundled — documents are not in the scope of this tutorial, but it’s helpful to know about them.

It’s your job to decode the data object and provide it to your app.

You’ll add code for these two methods, now.

Encoding the Document

First, add this import to the top of the file below the import UIKit statement:

import MarkupFramework

Next, add these variables to the MarkupDocument class:

static let defaultTemplateName = BottomAlignedView.name
static let filenameExtension = "rwmarkup"

var markup: MarkupDescription = ContentDescription(template: defaultTemplateName) {
  didSet {
    updateChangeCount(.done)
  }
}

The two type properties are constants that you’ll use in more than one place.

The markup property uses valid content as its default value. Each time you set this property, you update the change count so that UIDocument knows to save itself at appropriate times.

Now, replace the body of contents(forType:) with the following code:

let data: Data
do {
  data = try NSKeyedArchiver.archivedData(withRootObject: markup, requiringSecureCoding: false)
} catch {
  throw DocumentError.archivingFailure
}
guard !data.isEmpty else {
  throw DocumentError.archivingFailure
}
return data

This code encodes the current contents of the markup property using NSKeyedArchiver and returns it to UIDocument for saving to the file system.

Decoding the Document

For the decoding half, add this code to the body of load(fromContents:ofType:):

// 1
guard let data = contents as? Data else {
  throw DocumentError.unrecognizedContent
}

// 2
let unarchiver: NSKeyedUnarchiver
do {
  unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
} catch {
  throw DocumentError.corruptDocument
}
unarchiver.requiresSecureCoding = false
let decodedContent = unarchiver.decodeObject(of: ContentDescription.self,
                                             forKey: NSKeyedArchiveRootObjectKey)
guard let content = decodedContent else {
  throw DocumentError.corruptDocument
}

// 3
markup = content

In this method, you do the following:

  1. Confirm that the contents are an instance of Data.
  2. Decode that data as a ContentDescription using NSKeyedUnarchiver.
  3. Store that object so that it is ready to use in the rest of the module.

That’s all you need to do to create a basic UIDocument subclass.

Build the project just to check that everything compiles.

Installing UIDocumentBrowserViewController

In this section, you’ll add code to present a UIDocumentBrowserViewController and connect its associated delegate UIDocumentBrowserViewControllerDelegate.

Open the folder Markup/Primary Views in Project navigator. Open RootViewController.swift.

Presenting a Container View Controller

DocumentBrowserViewController is a stub that is provided in the starter app project; it is limited to keep you focused on the tutorial content. It acts as a container for UIDocumentBrowserViewController.

First, add this variable to the RootViewController class:

lazy var documentBrowser: DocumentBrowserViewController = {
  return DocumentBrowserViewController()
}()

This will allow you to create a DocumentBrowserViewController when it’s needed.

Add this method to the main class of RootViewController:

func displayDocumentBrowser(inboundURL: URL? = nil, importIfNeeded: Bool = true) {
  if presentationContext == .launched {
    present(documentBrowser, animated: false)
  }
  presentationContext = .browsing
}

In this code, if this is the initial launch, you present the DocumentBrowserViewController modally.

Later in this tutorial, you will use the two parameters in the method to handle incoming URLs, but don’t worry about them right now.

Finally, find the method viewDidAppear(_:) and replace:

displayMarkupController(presenter: self)

with:

displayDocumentBrowser()

Build and run. You should see a green background appear:

Installed Document Browser VC

Success!

Configuring UIDocumentBrowserViewController

Now that you’ve pushed an empty modal view onto the screen, next you’ll learn how to display the built-in user interface for the document browser.

Open the folder Markup/UIDocument Mechanics in Project navigator. Open DocumentBrowserViewController.swift.

Add this code to the main class of DocumentBrowserViewController:

var browserDelegate = DocumentBrowserDelegate()
lazy var documentBrowser: UIDocumentBrowserViewController = {
  let browser = UIDocumentBrowserViewController()
  
  browser.allowsDocumentCreation = true
  browser.browserUserInterfaceStyle = .dark
  browser.view.tintColor = UIColor(named: "RazeGreen") ?? .white
  browser.delegate = browserDelegate
  
  return browser
}()

func installDocumentBrowser() {
  view.pinToInside(view: documentBrowser.view)
}

In viewDidLoad(), replace:

view.backgroundColor = UIColor(named: "RazeGreen")

with:

installDocumentBrowser()

In this code, you:

  • Create an instance of DocumentBrowserDelegate.
  • You then create an instance of UIDocumentBrowserViewController, configure it with some properties and assign the delegate.
  • Lastly, you install the view of UIDocumentBrowserViewController inside DocumentBrowserViewController in viewDidLoad().

The key properties you’ve set on the view controller are:

  • allowsDocumentCreation is true. You want to be able to create documents.
  • browserUserInterfaceStyle is .dark. Delete this to use the default .light style.
  • tintColor is RazeGreen from Colors.xcassets because who doesn’t like Razeware Green?

Build and run. You’ll now see the UIDocumentBrowserViewController on launch:

unconfigured UIDocumentBrowserVC

There are no locations available yet. You’ll fix that next.

Configuring Info.plist

You can’t use UIDocumentBrowserViewController just by instantiating it. You need to add some key-value pairs to your Info.plist. These values inform iOS about the file types your app supports.

Open Markup/Info.plist from the Project navigator. Then, open Markup/Resources/Template.plist in the assistant editor by holding down Alt and clicking on Template.plist.

Spliscreen plist configuration

In Template.plist, there are three key-value pairs to add to Info.plist:

  • UISupportsDocumentBrowser notifies iOS that you want to use UIDocumentBrowserViewController.
  • CFBundleDocumentTypes is an array of dictionaries that defines the properties of the documents that your app will support.
  • UTExportedTypeDeclarations is an array of dictionaries that exposes the document properties to other apps and services on the device.

It’s possible to set these up manually in the info section of your target properties.

Info tab project setup

In this tutorial, you will copy and paste them into your Info.plist.

Copy/paste plist values

Select each one in turn from Template.plist and copy it (Command-C). Then click inside Info.plist and paste (Command-V). Click the images above for larger versions if you want to see more detail.

Build and run. Now, something cool happens. Select On My iPad from the Locations list and there will be a folder named Markup with the app icon on it. Open that folder. The document Default that you created at the beginning of this tutorial is there waiting for you:

App directory

Directory contents

Your app gets its own folder in Files, a special icon, and a new document button, just from adding those lines to your Info.plist. Next, you’ll make it all work.

Responding to UIDocumentBrowserViewController Delegate Actions

Most stock view controllers in iOS use a delegate to perform customization rather than encouraging subclassing. UIDocumentBrowserViewController is no exception.

In this section, you’ll configure a UIDocumentBrowserViewControllerDelegate to create a new document and open an existing document.

Open the folder Markup/UIDocument Mechanics in Project navigator. Find DocumentBrowserDelegate.swift.

DocumentBrowserDelegate conforms to UIDocumentBrowserViewControllerDelegate. It provides empty implementations of four optional delegate methods:

  1. documentBrowser(_:didRequestDocumentCreationWithHandler:) is called when you select Create Document in the browser UI.
  2. documentBrowser(_:didPickDocumentURLs:) is called when you select an existing document in the browser.
  3. documentBrowser(_:didImportDocumentAt:toDestinationURL:) informs the delegate that a document has been imported into the file system.
  4. documentBrowser(_:failedToImportDocumentAt:error:) informs the delegate that an import action failed.

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

Allowing Other Apps to Open Documents

Along with UIDocumentBrowserViewController, iOS 11 introduced the Files app to allow you to browse the file system on your device. Files allows you to open documents from anywhere on the device’s file system.

In this section, you’ll give Markup the ability to handle open events from Files or any other app.

Setting Up the App Delegate

When a request comes through to open a Markup document from outside the app, you won’t be surprised to discover that UIApplication makes a call to a protocol method on the UIApplicationDelegate.

iOS sends the Markup app the inbound URL. You need to pass the URL down the control chain to the UIDocumentBrowser instance:

open in place url flow

Updating DocumentBrowserViewController

In this section, you’ll give the inbound URL to UIDocumentBrowserViewController for handling.

Open DocumentBrowserViewController.swift from Markup/UIDocument Mechanics and add this extension to the end of the file:

extension DocumentBrowserViewController {
  func openRemoteDocument(_ inboundURL: URL, importIfNeeded: Bool) {
    documentBrowser.revealDocument(at: inboundURL, importIfNeeded: importIfNeeded) { url, error in
      if let error = error {
        print("import did fail - should be communicated to user - \(error)")
      } else if let url = url {
        self.openDocument(url: url)
      }
    }
  }
}

This method takes the two arguments that you will pass along from AppDelegate by RootViewController and gives them to the UIDocumentBrowserViewController instance. Assuming the revealDocument(at:importIfNeeded:completion:) call is successful, the app opens the URL.

Updating RootViewController

Here, you’ll make a change to RootViewController so that it can handle the inbound URL from AppDelegate.

Open RootViewController.swift from Markup/Primary Views.

Add this extension in RootViewController.swift.

extension RootViewController {
  func openRemoteDocument(_ inboundURL: URL, importIfNeeded: Bool) {
    displayDocumentBrowser(inboundURL: inboundURL, importIfNeeded: importIfNeeded)
  }
}

The method openRemoteDocument(_:importIfNeeded:) forwards the parameters to displayDocumentBrowser .

Now, find displayDocumentBrowser(inboundURL:importIfNeeded:) in the main class.

Add the following code after the line presentationContext = .browsing:

if let inbound = inboundURL {
  documentBrowser.openRemoteDocument(inbound, importIfNeeded: importIfNeeded)
}

The parameters are passed along the chain to the DocumentBrowserViewController instance.

Updating AppDelegate

Open the folder Markup/Infrastructure and then open AppDelegate.swift.

The protocol method that you need to react to is application(_:open:options:).

This method is called after the call to application(_:didFinishLaunchingWithOptions:) in the event that an app launch is triggered.

Add this method to the body of the AppDelegate class:

func application(_ app: UIApplication, open inputURL: URL, 
                 options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
  
  // 1
  guard inputURL.isFileURL else {
    return false
  }
  
  // 2
  guard let rootController = window?.rootViewController as? RootViewController else {
    return false
  }
  
  // 3
  rootController.openRemoteDocument(inputURL, importIfNeeded: true)
  return true
}

This method does the following:

  1. Checks if the URL is a file URL like file://foo/bar/mydoc.rwmarkup . You aren’t interested in HTTP URLs for this case.
  2. Gets the RootViewController instance.
  3. Sends the inbound URL and boolean down the chain to RootViewController.

Build and run. If you haven’t done so already, take the time to create at least two documents.

In the Simulator menu, choose Hardware > Home. Open the Files app. Try to open documents from the Markup folder. Go back and try opening a different document while another is open.

Well done! Your app is now a good citizen of the iOS file system.

Providing a Custom Document Icon

Right now, the documents that you create take their icon from the AppIcon asset. To see the contents of a document, you need to open it. What if you could provide a preview of the document content in the icon?

In this section, you’ll learn how to create a ThumbnailProvider extension.

Adding a ThumbnailProvider Extension Target

Select the Markup project in the Project navigator.

Click the + button in the target list:

Select iOS >Application Extension >Thumbnail Provider in the template list:

add thumbnail provider extension

Name the target MarkupThumbnail and click Finish to commit the changes:

configure the extension

You will see a prompt asking if you’d like to activate the new scheme. Click Cancel. For this tutorial, instead of testing the thumbnail by itself, you’ll check to see if it’s working by running the app.

Configuring a QLThumbnailProvider Subclass

In the Project navigator, open the new folder MarkupThumbnail that has appeared. Open ThumbnailProvider.swift.

The template code that Xcode provides is a subclass of QLThumbnailProvider with the one method that needs to be overridden already in place: provideThumbnail(for:_:).

iOS will make a call to that method with a QLFileThumbnailRequest. Your job is to call the handler closure with an instance of QLThumbnailReply:

Thumbnail generation cycle

QLThumbnailReply has three possible init methods. You’ll be using init(contextSize:currentContextDrawing:).

The currentContextDrawing parameter allows you to supply a drawing block. You use the drawing instructions like you would use in the draw(_:) method of UIView. You work in a UIKit-style coordinate system.

First, import MarkupFramework into the extension. Add this line just below import QuickLook:

import MarkupFramework

The need for sharing code with the extension is the reason you have the separate framework for the core model and drawing classes.

Delete everything that Xcode provided inside the body of provideThumbnail.

Insert this code into the body:

handler(QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
  
  var result = true
  do {
    // 1
    let data = try Data(contentsOf: request.fileURL)
    let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
    unarchiver.requiresSecureCoding = false
    if let content = unarchiver.decodeObject(of: ContentDescription.self,
                                             forKey: NSKeyedArchiveRootObjectKey) {
      
      // 2
      let template = PluginViewFactory.plugin(named: content.template)
      
      // 3
      template.view.frame = CGRect(origin: .zero, size: request.maximumSize)
      template.update(content)
      
      // 4
      template.view.draw(template.view.bounds)
      
    } else {
      result = false
    }
  }
  catch {
    result = false
  }
  
  return result
}), nil)

Here’s what’s happening:

  1. The QLFileThumbnailRequest has added the URL to the file as a property. You use that URL to unarchive the ContentDescription object.
  2. You instantiate an instance of PluginView using the template information from the content.PluginView that was supplied by the starter project.
  3. PluginView has a UIView object that you then configure with the size information from the QLFileThumbnailRequest.
  4. You then call the draw(_:) method to draw the UIView right into the current drawing context.

That’s all you need to do from the drawing side.

Configuring the Info.plist

How does iOS know that this Thumbnail Provider should be used for Markup files? It gathers that information from the Info.plist in the extension.

Open MarkupThumbnail/Info.plist.

Next, expand NSExtension / NSExtensionAttributes / QLSupportedContentTypes:

extension plist configuration

Add one element to the QLSupportedContentTypes array.

Now, set that element as type String and value:

com.razeware.rwmarkup

The UTI, com.razeware.rwmarkup, is the same one that you used in CFBundleDocumentTypes and UTExportedTypeDeclarations in the main app. iOS now knows to use this QLThumbnailProvider for files with the extension rwmarkup.

Linking the Framework

The last thing to do is to link the MarkupFramework to the extension:

link framework target

  1. Expand the Products folder in the Project navigator.
  2. Select MarkupFramework.framework.
  3. Add a check to MarkupThumbnail in the Target Membership of the file inspector.

You may need to set your run target back to Markup after adding the Thumbnail Provider extension.

Build and run. Wait a few seconds for the extension to boot and do its work. The icons should turn into baby representations of the content:

browser thumbnails

Pretty cool, right?

Where to Go From Here?

Congratulations! You have built a document-based app using the system file browser. You can find a reference finished project via the Download Materials link at the top or bottom of this tutorial.

In this tutorial, you’ve learned how to:

  • Create a UIDocument subclass.
  • Configure file properties and UTI’s.
  • Interact with the system file browser component.
  • Handle interactions with other apps.
  • Supply a dynamically generated icon.

The advantage of this component is that everyone who uses this interface will recognize it as a file system. You can share these documents and put them on your iCloud drive for editing on any of your devices. If you’re upgrading an existing code base, and can drop iOS 10 support, now might be a good time to delete some code and replace it with this component.

Some areas for further research:

I look forward to hearing about your adventures with UIDocumentBrowserViewController and its friends in the forum below!

Average Rating

4.5/5

Add a rating for this content

2 ratings

Contributors

Comments