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 4 of 5 of this article. Click here to view the first page.

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.