I have been working on an artist utility app, with the primary purpose to present an image and super-thin grid overlay. The inspiration came from the cropping functionality in the Photos app – but that’s very ephemeral to a the act of croping an image, and isn’t easily viewable on a continued basis (such as on an iPad) when you want that grid to support your sketching or painting. Using a grid like this is done for a couple of purposes: one of which is the “process by Leonardo” for helping to capturing and copying an image by hand. The other to double check the framing and composition against what’s called The Rule of Thirds.
I originally didn’t think of this as an application that would have or use a document format, but after trying it out a bit and getting some feedback on the current usage, it became abundantly clear that it would benefit tremendously by being able to save the image and the framing settings that shows the grid overlay. So naturally, I started digging into how to really enable this, which headed directly towards UIDocument.
Using UIDocument pretty quickly begged the question of supporting a viewer for the files, which led to researching UIDocumentBrowser, which was a rather surprisingly invasive design change. Not bad, mind you – just a lot of moving parts and new concepts:
- UIDocument instances are asynchronous – loading and saving the contents is separate from instantiating the document.
- UIDocument’s support cloud-hosted services from the get-go – which means they also include a concept of states that might be surprising including
editingDisabledin addition to reflecting loading, saving, and error conditions while doing these asynchronous actions.
- UIDocument is built to be subclassed, but how you handle tracking the state changes & async is up to you.
- UIDocumentBrowser is built to be controlled through a delegate/controller setup and UIDocumentControllerViewController which is subclassed, and also demands to be root of the view hierarchy.
Since my document data included UIImage and UIColor, both of which are annoying when trying to persist them using struct coding with swift , I ended up using NSKeyedArchiving, and then later NSSecureCoding, to save out the document.
One of the first lesson I barked my shin on here was when I went to make a ThumbnailPreview extension that loaded the document format and returned a thumbnail for the document icon. The first thing I hit was that NSKeyedArchiving was failing to load/decode the contents of my document when attempting to make the thumbnail, while the application was able to load and save the document just fine. It likely should have been more obvious to me, but the issue has to do with how NSKeyedArchiving works – it decodes by class name. In the plugin, the module name was different – so it was unable to load the class in question, which I found out when I went to the trouble of adding a delegate to the NSUnarchiver to see what on earth it was doing.
One solution might have been to add in some translation on NSKeyedUnarchiver to translate the class to the module name that was associated with the plugin
setClass(_:forClassName:). I took the different path of taking the code that represented my document model and breaking it out into it’s own framework, embedded within the application – and then imported that framework into the main application and the preview plugin as well.
UIDocument Lesson #1: it may be worth putting your model code into a framework so plugins and app extensions can use it.
The second big “huh, I didn’t think of that…” was in using UIDocument. Creating a UIDocument and loading its data are two very separate actions, and a UIDocument actually has quite of bit of state that it might be sharing. The DocumentBrowser sample code took the path of making an explicit delegate structure to call back as things loaded, which I ended up adopting. The other sample code that Apple provided (Particles) was a lot easier to start with and understand, but doesn’t really do anything with the more complex world of handling saving and loading, and the asynchronous calls to set all that up.
UIDocument Lesson #2: using a document includes async calls to save, load and states that represent even potential conflicts when the same doc is editing at the same time from different systems.
One particularly nice little feature of UIDocument is that it includes a Progress property that can be handed and set on the UIDocumentBrowser’s transition controller when you’ve selected a document, so you get a nice bit of animation as the document is loaded (either locally or from iCloud).
UIDocumentBrowser Lesson #1: the browser subclass has a convenient (but not obvious) means of getting a animated transition controller for use when opening a document – and you can apply a UIDocument’s Progress to show the loading.
The callbacks and completions were the trickiest to navigate, trying to isolate which view controller had responsibility for loading the document. I ended up making some of my own callbacks/completion handlers so that when I was setting up the “editor” view I could load the UIDocument and handle the success/failure, but also supplied the success/failure from that back to the UIDocumentBrowserViewController subclass I created to support the UIDocumentBrowser. I’m not entirely convinced I’ve done it the optimal way, but it seems to be working – including when I need to open the resulting document to create a Quicklook Thumbnail preview.
The next step will be adding an IOS Action Extension, as that seems to be the only real way that you can interact with this code directly from Photo’s, which I really wanted to enable based on feedback. That will dovetail with also allowing the application to open image based file URLs and create a document using that image file as its basis. The current workflow for this application is creating a new document, and then choosing an image (from your photo library), so I think it could be significantly simpler to invoke and use.