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?
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:
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:
- Checks if the URL is a file URL like
file://foo/bar/mydoc.rwmarkup
. You aren’t interested in HTTP URLs for this case. - Gets the
RootViewController
instance. - 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:
Name the target MarkupThumbnail and click Finish to commit the changes:
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
:
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:
- The
QLFileThumbnailRequest
has added the URL to the file as a property. You use that URL to unarchive theContentDescription
object. - You instantiate an instance of
PluginView
using the template information from thecontent.PluginView
that was supplied by the starter project. -
PluginView
has aUIView
object that you then configure with the size information from theQLFileThumbnailRequest
. - 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:
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.