Get Your Architecture Right, Because You Always Have More Time than You Think You Do
When facing an anxiety-provoking deadline for a software project, you have more time to plan your architecture than it may seem. Indeed, you should consider near and medium term requirements and risks to the full extent that it is possible to consider them given current knowledge, even if you choose not address any of them up front. Take only calculated risks. Factor those risks carefully into your initial implementation. Do not touch a keyboard until you have done so. Cut corners, but cut them thoughtfully.
Urgency Versus Anxiety
It's worth noting the important difference between a sense of urgency and anxiety. Before I got into software development I was a registered nurse in an ICU. One evening a patient went into cardiac arrest. In an instant, the room filled with nurses and other folks eager to jump in and help. I was leaning over the patient's bed giving chest compressions to keep the patient's blood flowing. I felt myself swarmed by a small crowd in scrubs and Crocs. There were more people present than necessary, and it made the atmosphere in the room ratchet up from an appropriate urgency to a palpable anxiety. A supervising physician on the scene wisely ordered everyone not currently providing care to leave the room. As the excess folks filed out, I overheard the physician mention something to a colleague about the dangerous anxiety he was correcting:
I'll never forget something an instructor told me in med school about situations like this, "You always have more time than you think you do."
He wasn't addressing me directly, but the lesson stuck: there will never be a medical situation so dire that you literally cannot spare a moment to consider an appropriate course of action. There's no use for anxiety in the mind of a professional doing his or her duty in a crisis. March all the unnecessary anxious thoughts out of your mind and make room for a deliberate response. Give yourself permission to think. In the years since that day, I've found this lesson to be very valuable, even outside of healthcare. Strange as it may seem, I hear echoes of it in my process for sketching out architectural roadmaps for the applications I work on.
(Fr)agile
In an ideal world, agile processes are adhered to with perpetual regularity, pulsing in a cadence of small, iterative changes. In the real world, an organization that can unwaveringly adhere to an agile process is hard to come by. Customer demands, public events, and other factors create constraints that require setting a fixed ship date for a product launch. This is lethal to an agile process because there's no margin of error for iteration. You don't have the luxury of repeated revisions. You barely have time to ship your first draft. Under these conditions, the anxiety of the engineers on such a project skyrockets. Facing a tall list of requirements and a fast-approaching, narrow delivery window, there is a temptation to bust out the keyboards and hammer out some code because how will we ever finish unless we can show immediate and significant progress oh god oh god. Invariably, code written in thoughtless haste is unmaintainable or, worse, unshippable. Technical debt is accumulated at an unacceptable rate. Inappropriate patterns are chosen and implemented haphazardly.
Breaking it Down
It is difficult to break a down a set of large problems into atomic problem units which can be distributed among a team of developers and solved in parallel. In a healthy agile process, there is no single delivery date, but an ongoing process of experimentation and refinement. Impedance mismatches between the output of developers working on separate components are addressed through repeated course corrections. You fully roll out a feature only when it's ready to be. But when there's an aggressive and fixed delivery date, there's no room in the process for such refinements. Each component has to be shippable in its first iteration, and it has to immediately lock into place alongside all the other components.
Under the pressure of a looming deadline, developers may spend an inadequate amount of time considering their architectural roadmap. At worst, this leads to a code base that fails to satisfy the launch-day product requirements on time. At best, the code produced is ill-suited for the life of the product immediately after launch. There's no agile process in place to carry it through future milestones, so the cycle of fixed delivery deadlines and frantic architectural changes repeats until the product fails.
Here's a metaphor for the problem. Consider an illustrator tasked with drawing a human figure. A trained illustrator works like this:
She begins with gesture lines and primitive shapes, blocking out the pose, proportions, and perspective. Progressive levels of detail are added, guided by those initial lines and shapes, until the drawing arrives at its intended appearance. Inexperienced artists try to begin at the end, drawing body contours without the aid of any primitive elements, or they hastily jot down the gesture lines and shapes without regard for proportion and perspective. Either way the result is unsatisfactory.
Carrying the metaphor, what I have seen anxious developers do is start with the far right drawing without any gesture lines. They task team members with drawing each limb separately and at a premature level of detail. When at last the team attempts to pin the components together the perspectives don’t match, the proportions are childish, and the result is hideously unusable. The irony is that — just as a rough pass of detail over an expertly-arranged set of gesture lines can yield a pleasantly unfinished portrait — a simple overlay of features and polish atop an expertly-ordered primitive architecture is the very definition of a minimally-viable product.
There's another software development pitfall suggested by this metaphor. Accurate and pleasant gesture lines are extraordinarily difficult to master. They may look like stick figures to an untrained eye, but they're anything but. Countless hours of practice and studious observation are required to become proficient at drawing these primitive shapes. If you undertake them without care, the resulting drawing will have all the same flaws as a drawing made without any gesture lines. In the same way, an architectural roadmap must be considered with extreme care. Don't just list everything you know, list everything you don't or can't know. You don't have to plan every detail, but you must wrestle with the problem area long enough to be reasonably confident that your architecture will be both efficient in the short term and stable for the medium term. If you're lucky it will be stable for the long term. No matter what you choose, it'll always be a guess. But make it a well-educated guess.
A Concrete Example
Here’s a concrete example of the kind of discussion I think can be spared some time at the beginning of a project without making commitments that over- or under- engineer things. Consider an app backed by a web service with user-specific accounts. Questions that might come up during a planning phase:
How likely do we think it is that the app will ever need to support more than one account at a time?
If we choose not to leave space for multiple accounts in our architecture, how disruptive would it be if multiple accounts suddenly became a requirement?
How much additional up-front effort would it take to leave space for multiple accounts in our architecture though we would only ship with user-facing support for a single account?
How likely is it that we’ll have to support iOS State Restoration, and would this be impacted by our chosen account plan?
What else haven’t we considered, and is any of it risky enough to require addressing now?
And the key points during that discussion might be:
We have no idea how likely it is we’ll need to support multiple accounts. All we know is it’s not currently required.
If we think we’ll never have to support multiple accounts, one option is to provide global access to a singleton instance of an account.
If we suddenly have to support multiple accounts and we’re using a singleton instance fixed to one account, that requirement change would be very painful to support.
Passing an isolated account via dependency injection instead of providing a globally-accessible singleton instance would be comparatively easier to migrate to a multiple-account setup.
Passing an isolated account via dependency injection would have a trivial impact on overall level of effort in a single-account application.
Dependency injection could conceivably make supporting iOS State Restoration harder as that API is based on isolated view controllers re-instantiating themselves via NSCoding. Passing references to specific account instances during or after state restoration is considerably more complex than if restored view controllers had immediate access to a global instance during decoding.
Please note I’m not arguing for one way of the other here. I'm merely sketching out some terrain over which such a discussion might traverse.
Conclusion
In the end there’s always risk. You make the best choice you can given the information you have. I recommend discussing at length both the near and medium term before comitting to a near-term plan. All too often, these discussions either don’t happen or they happen in a rush and so risks aren’t considered to the full extent that it is possible to consider them given current knowledge.
That last line is the bad habit that rubs me wrong:
the risks aren’t considered to the full extent that it is possible to consider them given current knowledge.
This is the point of the quote from that ICU physician I admired so much. You always have more time than it seems like you do. You always have time to consider the impact of what you know and what you don’t, even if you choose not to address any of the risks up front, even if the outcome of that consideration means cutting huge corners. At least the risks you’re taking are calculated.
4 notes
·
View notes
Document-Based Apps Tutorial: Getting Started
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.
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:
Xcode 10 or higher.
Intermediate Swift skills.
A basic understanding of delegation patterns and protocol-oriented programming.
A basic understanding of do/try/catch and Error types.
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.
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:
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:
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.
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:
Data for when your data is a binary blob. You’ll be using this format in this tutorial.
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:
Confirm that the contents are an instance of Data.
Decode that data as a ContentDescription using NSKeyedUnarchiver.
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:
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:
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.
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.
In this tutorial, you will copy and paste them into your Info.plist.
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:
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:
documentBrowser(_:didRequestDocumentCreationWithHandler:) is called when you select Create Document in the browser UI.
documentBrowser(_:didPickDocumentURLs:) is called when you select an existing document in the browser.
documentBrowser(_:didImportDocumentAt:toDestinationURL:) informs the delegate that a document has been imported into the file system.
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:
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.
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 the ContentDescription object.
You instantiate an instance of PluginView using the template information from the content.PluginView that was supplied by the starter project.
PluginView has a UIView object that you then configure with the size information from the QLFileThumbnailRequest.
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.
Linking the Framework
The last thing to do is to link the MarkupFramework to the extension:
Expand the Products folder in the Project navigator.
Select MarkupFramework.framework.
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:
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:
NSFilePresenter is the core protocol that UIDocument adopts to interact with the file system.
QLPreviewingController is the class you need to implement to make a Quicklook Preview Extension.
WWDC 2017 Session 229 gives an overview of this topic. It covers more advanced concepts, too.
I look forward to hearing about your adventures with UIDocumentBrowserViewController and its friends in the forum below!
The post Document-Based Apps Tutorial: Getting Started appeared first on Ray Wenderlich.
Document-Based Apps Tutorial: Getting Started published first on https://medium.com/@koresol
0 notes