#UIDocument
Explore tagged Tumblr posts
jacob-cs · 7 years ago
Link
original source : https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457-CH2-SW1
The Role of View Controllers
View controllers are the foundation of your app’s internal structure. Every app has at least one view controller, and most apps have several. Each view controller manages a portion of your app’s user interface as well as the interactions between that interface and the underlying data. View controllers also facilitate transitions between different parts of your user interface.
Because they play such an important role in your app, view controllers are at the center of almost everything you do. The UIViewController class defines the methods and properties for managing your views, handling events, transitioning from one view controller to another, and coordinating with other parts of your app. You subclass UIViewController (or one of its subclasses) and add the custom code you need to implement your app’s behavior.
There are two types of view controllers:
Content view controllers manage a discrete piece of your app’s content and are the main type of view controller that you create.
Container view controllers collect information from other view controllers (known as child view controllers) and present it in a way that facilitates navigation or presents the content of those view controllers differently. (content를 다루지 않는다. root view만 관리한다)
Most apps are a mixture of both types of view controllers.
View Management
The most important role of a view controller is to manage a hierarchy of views. Every view controller has a single root view that encloses all of the view controller’s content. To that root view, you add the views you need to display your content. Figure 1-1 illustrates the built-in relationship between the view controller and its views. The view controller always has a reference to its root view and each view has strong references to its subviews.
Tumblr media
NOTE
It is common practice to use outlets to access other views in your view controller’s view hierarchy. Because a view controller manages the content of all its views, outlets let you store references to the views that you need. The outlets themselves are connected to the actual view objects automatically when the views are loaded from the storyboard.
A content view controller manages all of its views by itself. A container view controller manages its own views plus the root views from one or more of its child view controllers. The container does not manage the content of its children. It manages only the root view, sizing and placing it according to the container’s design. Figure 1-2 illustrates the relationship between a split view controller and its children. The split view controller manages the overall size and position of its child views, but the child view controllers manage the actual contents of those views.
Tumblr media
For information about managing your view controller’s views, see Managing View Layout.
Data Marshaling
A view controller acts as an intermediary between the views it manages and the data of your app. The methods and properties of the UIViewController class let you manage the visual presentation of your app. When you subclass UIViewController, you add any variables you need to manage your data in your subclass. Adding custom variables creates a relationship like the one in Figure 1-3, where the view controller has references to your data and to the views used to present that data. Moving data back and forth between the two is your responsibility.
Tumblr media
You should always maintain a clean separation of responsibilities within your view controllers and data objects. Most of the logic for ensuring the integrity of your data structures belongs in the data objects themselves. The view controller might validate input coming from views and then package that input in the format that your data objects require, but you should minimize the view controller’s role in managing the actual data.
A UIDocument object is one way to manage your data separately from your view controllers. A document object is a controller object that knows how to read and write data to persistent storage. When you subclass, you add whatever logic and methods you need to extract that data and pass it to a view controller or other parts of your app. The view controller might store a copy of any data it receives to make it easier to update views, but the document still owns the true data.
User Interactions
View controllers are responder objects and are capable of handling events that come down the responder chain. Although they are able to do so, view controllers rarely handle touch events directly. Instead, views usually handle their own touch events and report the results to a method of an associated delegate or target object, which is usually the view controller. So most events in a view controller are handled using delegate methods or action methods.
For more information about implementing action methods in your view controller, see Handling User Interactions. For information about handling other types of events, see Event Handling Guide for iOS.
Resource Management
A view controller assumes all responsibility for its views and any objects that it creates. The UIViewControllerclass handles most aspects of view management automatically. For example, UIKit automatically releases any view-related resources that are no longer needed. In your UIViewController subclasses, you are responsible for managing any objects you create explicitly.
When the available free memory is running low, UIKit asks apps to free up any resources that they no longer need. One way it does this is by calling the didReceiveMemoryWarning method of your view controllers. Use that method to remove references to objects that you no longer need or can recreate easily later. For example, you might use that method to remove cached data. It is important to release as much memory as you can when a low-memory condition occurs. Apps that consume too much memory may be terminated outright by the system to recover memory.
Adaptivity
View controllers are responsible for the presentation of their views and for adapting that presentation to match the underlying environment. Every iOS app should be able to run on iPad and on several different sizes of iPhone. Rather than provide different view controllers and view hierarchies for each device, it is simpler to use a single view controller that adapts its views to the changing space requirements.
In iOS, view controllers need to handle coarse-grained changes and fine-grained changes. Coarse-grained changes happen when a view controller’s traits change. Traits are attributes that describe the overall environment, such as the display scale. Two of the most important traits are the view controller’s horizontal and vertical size classes, which indicate how much space the view controller has in the given dimension. You can use size class changes to change the way you lay out your views, as shown in Figure 1-4. When the horizontal size class is regular, the view controller takes advantage of the extra horizontal space to arrange its content. When the horizontal size class is compact, the view controller arranges its content vertically.
Tumblr media
Within a given size class, it is possible for more fine-grained size changes to occur at any time. When the user rotates an iPhone from portrait to landscape, the size class might not change but the screen dimensions usually change. When you use Auto Layout, UIKit automatically adjusts the size and position of views to match the new dimensions. View controllers can make additional adjustments as needed.
For more information about adaptivity, see The Adaptive Model.
다음 내용 ) https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/TheViewControllerHierarchy.html#//apple_ref/doc/uid/TP40007457-CH33-SW1
0 notes
cdemiranda · 6 years ago
Link
via Ray Wenderlich
0 notes
iyarpage · 7 years ago
Text
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
icd11559 · 7 years ago
Link
via Notes Feeds
0 notes
objpost · 10 years ago
Text
iCloud and them UIDocuments : Part 2
In this part of iCloud & UIDocument syncing, I'll be covering two very important topics: Change sets & Conflict handling. If you're not sure what these are, go ahead and read the first part.
Change Sets
In the first part, I spoke about Change Sets. As you know, these change sets are regular NSObject subclassed objects, with the following, self-descriptive properties:
@interface EFChangeSet : NSObject <NSCoding> @property (nonatomic, assign) enum EFActionType actionType; @property (nonatomic, copy) NSMutableDictionary *changes; // ignore, if actionType is delete @property (nonatomic, copy) NSString *device; @property (nonatomic, copy) NSDate *date; @property (nonatomic, assign) enum EFObjectType objectType; @property (nonatomic, copy) NSString *objectUUID; // optional, pass if actionType is delete @end
Everytime, setData: is invoked on a cluster, it creates such changesets if any data has changed: Additions, modifications or deletions. It also stores which exact properties have changed, if any, and their new values in the event of a modification. All such changesets are then added to the changes array housed by the cluster itself. This is also synced along with the data.
Quite often, changesets older than 5 days are evicted from the array. This ensures that the cluster doesn't bloat up and occupy unneccesary space on the device's disk.
An interesting point to note is the device property. This is a unique ID assigned to every device, which ensures that the same device doesn't repeat the changes on it's dataset. Every other device should follow the instructions on how to immitate those changes on it's own dataset.
But what if the dataset the device now has, doesn't not match the final dataset it's expected to have? Well, we now enter the deadly, enigmatic, and quite frankly, the most cumbersome part of working with UIDocuments: Conflict Handling.
Conflicts
For the ones who don't know what these conflicts are, here's a brief summary from Apple's Documentation
In an iCloud world, when a user has installed a document-based application on multiple devices or desktop systems, there can be conflicts between different versions of the same document. Recall that an application updates a document file in the local container directory and those changes are then transmitted—usually immediately—to iCloud. But what if this transmission is not immediate? For example, you edit a document using the Mac OS X version of your application, but you’ve also edited the same document using the iPad version of the application—and you did so while the device was in Airplane Mode. When you switch off Airplane Mode, the local change to the document is transferred to iCloud. iCloud notices a conflict and notifies the application.
However, Esfresco's architecture of the changesets revealed another possible conflict stage which I mentioned above. So, all of Apple's defined states, and with Esfresco's own, I had quite a task infront of me. This is another area where iCloudDocumentSync took over all control, and usually left me with corrupted documents.
If you read Apple's documentation on the subject, it's very sparse and for a good reason. How you architect your UIDocument is up to you. So there is no "one right way" to handle these conflicts. Luckily, Apple makes two things easy: - Knowing when a document in a conflicted state - Marking the document as resolved, so your app can go back to doing what it does best.
Between those two, is a step: Actually handling the conflict. How you do it, as I mentioned, depends on your document's architecture. For Esfresco, it was no walk in the park.
Once a document goes into a conflicted state, the cluster immediately stops all write access to it's dataset. No changesets are created, and no modification timestamps are updated. It's world stops spinning. At this junction, the app makes an educated guess whether this conflict has happened due to the same document having several different copies (one from your work phone, and the other from your home phone), or due to changesets. Once it has determined that, it becomes simpler. If it's the former, I replicate the changes not made by this device onto it's own dataset, and then compare. Here, two things can happen: everything's good, or we have a changeset conflict.
In the event of a changeset conflict, things start getting real messy. The method that handles this state initially took 3s for 300 changesets. Now that was an immediate concern. A lot can happen in 3s on the newer iPhones. It's an incredible amount of time and the app couldn't be frozen. So to overcome this, all write releated operations are now queued, and resumed when the document is unlocked again. That solves one problem. I refactored the code over several days to get this going faster, till I reached a point where 300 changesets took half a second. Not too bad, I said, and resumed work on actually solving the conflicts. Esfresco has been designed to take a lot of educated guesses at this point. We've tried to cover a lot of edge-cases, but I'm sure we haven't covered it all.
Now for a quick fact: the changeset conflict resolution method is the largest method in Esfresco's codebase. It's also one of the most dangerous methods. What happens after it has finished processing is either a healthy document, or something that needs to be deleted and at the end of it, we're left with a confused, possibly angry customer. Yet, so far, we haven't come across a deleted document. To avoid this from ever happening, Esfresco notifies the user, uses the latest available dataset and continues.
If you aren't bored or scared by this yet (and I hope you aren't scared, there's no reason to be, no matter how daunting this all sounds), now comes the easy part: the last step. At this point, your document is in either have one of the possible situations: - Your changesets have matched, your data is good, and your current document is healthy. You can simply dismiss all the other conflicted versions and resume. - Or your have two doucments, between which you need to make a choice. Once you have, you tell the OS to use one, and you're done.
Yep, that's all that there's to it.
Closing Notes
Had I known all of this before starting work on implementing iCloud in Esfresco, would I still do it? YES It feels incredible to get down and dirty with all the nitty-gritties of your app. By the end of it, you'll end up knowing every nook and corner of your app. Bring on the trivia-saturday-nights (yeah, I don't do those, but just saying).
I see a lot of engineers here in India recommend alternatives to their managers simply to avoid all of this. Using a ready-to-go BaaS is usually their first preference. We avoided this path for a simple reason: You should own your data, and no one else. Using a BaaS would have resulted in your data being with a service provider, you don't know, or have never heard off. That is certainly not a good idea, especially with financial information.
Would I do this again, for a different app in the future? Why, yes! It's been an incredible time understanding the underlying archtecture of iCloud. But more so, implementing it, battling it out and rising to the top is even more satisfying, knowing you've done good, solid work. I hope more and more engineers adapt iCloud or CloudKit for it's simplicity (here come the CoreData folks!).
0 notes
attila · 12 years ago
Link
Justin Driscoll, on a very clean way to set-up a singleton to share a single UIDocument project-wide. I like.
In a side note, I hate filling up the app delegate. That's a recipe for bad re-use of code.
0 notes
objpost · 10 years ago
Text
iCloud and them UIDocuments
As many of you'll reading this must know, for the past few months, I've been working on Esfresco, Dezine Zync's first production app. Now that this app has been out for a bit, I'd like to share a couple of things I've learnt in the process. If you haven't checked out Esfresco, you should right away!
iCloud UIDocument syncing
iCloud UIDocument syncing has numerous articles on the web, spanning across the spectrum, each one with it's way of doing things. But, perhaps the best way of doing things for us in Esfresco was Apple's way. It's relatively simple to grasp, but that video doesn't cover actually implementing it. For that, you'll have to go through this document that explains a UIDocument's lifecycle.
So far, all good and dandy. But that's where the trouble starts. Apple's documentation on the subject is sparse, to say the least, and searching for solutions on stackoverflow didn't strike as a very great idea. If you do, you'll immediately notice how most of the accepted answers (or the ones marked as the right answer) are usually not very comprehensive. You can't blame the developers for this. The API for this is rather tricky to get a grasp on.
Alternatives
At this point, I was yearning for a simpler solution, possibly open-source, that we could use. I chanced upon iCloudDocumentSync by iRareMedia. It's one hellova Class that abstracts all the nasty little bits of working with UIDocument syncing into a neat little library. For the first two weeks, it was a joy to work it (and it still is, for simpler use-cases).
Then it was time to battle it out with the monster: Conflict handling. And that's where the weakness of this Class starts emerging. It isn't too bad. There's even this delegate method that gets called when there's a conflict on a certain UIDocument in the ubiquity container of your app:
- (void)iCloudFileConflictBetweenCloudFile:(NSDictionary *)cloudFile andLocalFile:(NSDictionary *)localFile;
Yet again, the problems began when testing conflicts. Since this Class abstracts away a lot of logic away to make the API simpler, it has to handle a lot of the nasty things associated with the subject. In doing so, it also makes some decisions for you like this one. Well, that was deal-breaker. And now I have to find another solution or work out my own.
Another day spent doing some research, and the best way to work this out was to write my own class.
One week later
It was done. No really, spending an entire week doing nothing but reading Apple's docs carefully, experimenting and putting all of that into Esfresco's core class, EFKit, actually proved to be fun. The trick was to really read between the lines and to think how an Apple Engineer would, when writing something millions of developers would use. At that point, it all became really easy.
Documents
Coming up with the right solution for Esfresco was a lot of trial and error. I don't think there was any better way. Sending a document to the ubiquity container so iCloudDaemon keeps it synced was the easy part. Bringing it back to the local storage wasn't a problem either. But as always, something will knick the neck while you're looking away.
UIDocument conflict resolution seems like it was written by a bunch of perfectly happy-with-their-lives Apple Engineers. Even the documentation on the subject is properly written. No reading between the lines, or "staring at it" through someone else's perspective. Within minutes, yes really, I had this bit sorted out. However, Apple's example in that page showed an example of letting the user select which version of the document to keep. This wasn't a possibility for Esfresco. Keeping the user oblivious of such things was necessary, as we've designed the app to be used by absolutely anyone and everyone. Assuming one knows about such technical things wasn't a risk we wanted to take. "Conflicted document what! Help me with this please" wasn't the subject of an email I ever wanted to read.
The only way left was to automatically handle conflicts, as even Apple recommends. But off course, they don't give an example of this (none that I know of at-least) as a UIDocument can handle anything that can be converted to NSData. More on this in a bit. What iCloudDocumentSync did to handle conflicts before you are notified of it, is compare files by date, bytes, etc. which is something you may not want. You may also want to handle merging data. This was another reason I had to drop using the class.
If you custom models (and I hope you do), you can simply implement methods conforming to the id<NSCoding> protocol and you're done. Nothing fancy there. You can even utilize NSKeyedArchiver and NSKeyedUnarchiver to combine multiple such models into a single NSData package and store it. Apple recommends we utilize multiple document clustering for this purpose, but that would make sense if each of those children documents had substantial non-interdependent data. In Esfresco's case, each of the documents have 3 properties:
@property (...) NSArray *changes; @property (...) id object; @property (...) NSDictionary *lastModified;
Each UIDocument subclass has it's own associated object, so that differs, but the idea is the same. Every time setObject: is invoked, the KVO notifies the subclass, which then creates change sets as required, and adds it to the changes array. It also updates the lastModified timestamp against the device's UUID. Every device gets it's own UUID when the app is launched for the first time. This makes handling change sets easier later on.
The EFKit class is responsible for delegating information from various controllers to the respective documents, moving them accordingly between the app's container and ubiquity container in a coordinated fashion. Now, I've tried using NSFileCoordinator for this, but it didn't work as effectively as I hoped it would. So instead, I created a serial thread, and all open, read, close and save operations are done on that thread. This way, I can queue these block calls, asynchronously, and handle the block invocations as they come in.
Once this was stabilized, and all bugs squashed, it was time to implement handling conflicts. (I'll cover this in part 2, as it's an extensive topic). But before you can even come to testing conflicts by forcefully creating them, you need to be able to load new changes on-the-fly as they come in from the iCloudDaemon.
Devices heating up!
At this point, I'd like to thoroughly appreciate how easy it is to work with NSMetaDataQuery and the NSMetaDataItem classes. Two notifications to observe and you'll be ready. However, I came across a catch, a weird behavior I doubt is documented. I'm going to find it again, and if not, file a documentation radar. As for the problem itself: the NSMetadataQueryDidUpdateNotification can fire very often. Sometimes, multiple times within a second. It's incredibly fast. So I thought of stopping the Query every time the associated method of that notification is invoked, and starting it again when I'm done. However, this started causing the CPU usage to always float around the 30% mark (first alarm), and my test device (an iPhone 6) started to heat up. I didn't notice this for almost an entire day (second alarm). Then after confirming the same behavior on an iPhone 5C and iPhone 5, I started digging through the code. it took a while to notice that this was the root cause. Turns out, stopping and starting the query multiple times was a bad idea. And for this very reason, Apple Engineers have given us the enableUpdates and disableUpdates methods. Swapped those in for the start and stop and every time was back to normal.
I suppose that's it for now. In the next part, I'll be covering conflict handling, and what to do with the change sets.
0 notes