Omni Frameworks Part 2: Using the Document Picker
Here’s part two of a series of posts chronicling my experiences using the Omni Frameworks. As before, I’d like to add the disclaimer that I don’t know if what I’ve done is best practice: the Omni Frameworks are not documented and I’m figuring things out as I go along.
Human readable Copyright
Here’s one tip for you: the Omni Frameworks seem to parse the copyright string in Info.plist. I got a few crashes where an assert failed. (Strangely this didn’t happen every time.) Make sure you have a human readable copyright string. This one worked for me:
Document types
We’re going to be building a document picker. This lets your users pick a document to edit, much like in Apple’s iWork apps. So we’re going to need to declare some document formats that the app supports. These settings are also in the Info tab. Here’s mine:
First I defined a document type. I gave it a name, and a UTI. Now, this UTI was for a custom document, so I made it up. If you wanted to support a standard document type like RTF or PDF, you’d need to use the UTI for that type.
I added the CFBundleTypeRole and LSHandlerRank keys because Omni’s sample had them in. I’m not sure what they’re for.
Next I had to declare the UTI. I put it in Exported UTIs because it’s one I made up. If it was an existing UTI, I’d put this information in Imported UTIs instead.
I added the same Description and Identifier. Then I made a dictionary key, UTTypeTagSpecification, under the additional properties section. In it I put public.mime-type (which again I made up), public.filename-extension (which is obviously the desired filename extension), and com.apple.ostype, which is a HFS Type Code. Type codes are four character strings that represented file types on the old MacOS. Use upper and lower case letters — ones that are all lower case are reserved by Apple. Again, I made this one up.
Creating new documents
We need to do a couple of main things to get the document picker to work. We’ll need a document class, to represent the file on disk. We’ll need a view controller that’ll handle displaying that document. And we’ll need some modifications to the App Delegate to tell it what types of document to create.
Start by making the two classes I mentioned. I made LocusDocument (inherits from OUIDocument), and LocusDocumentViewController (inherits from OUIViewController and conforms to OUIDocumentViewController).
Now, in the App Delegate, we can tell it what to create:
- (Class)documentClassForURL:(NSURL *)url; { return [LocusDocument class]; } - (NSString *)documentStoreDocumentTypeForNewFiles:(OFSDocumentStore *)store; { return @"com.locusapp.locus"; }
In the LocusDocument class, we then need to tell it how to make its view controller:
- (UIViewController *)makeViewController; { return [[LocusDocumentViewController alloc] init]; }
There are a couple of other methods to implement here. It needs to know how to read and write its contents to a file. For now, I’ll put stub methods in:
- (BOOL)readFromURL:(NSURL *)url error:(NSError **)outError; { return YES; } - (BOOL)writeContents:(id)contents toURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)saveOperation originalContentsURL:(NSURL *)originalContentsURL error:(NSError **)outError; { NSData *newData = [NSData data]; return [newData writeToURL:url atomically:NO]; }
Next, there are a couple of methods related to previews (i.e. the little thumbnails displayed in the document picker). This is code I copied out of Omni’s sample app with only minor changes: I’ve put it in a GitHub gist to save pasting lots of code here.
One more thing to mention: if you want to set up anything when a new document is created, override this method:
- (id)initEmptyDocumentToBeSavedToURL:(NSURL *)url error:(NSError **)outError;
The View Controller
Here’s what I put in the header file for LocusDocumentViewController:
@interface LocusDocumentViewController : OUIViewController<OUIDocumentViewController> @property(nonatomic) BOOL forPreviewGeneration; @property (nonatomic, weak) LocusDocument *locusDocument; @property (nonatomic, strong) UIToolbar *toolbar; @end
I gave it three properties. First, a BOOL to show whether this view controller was created just in order to generate a preview thumbnail. This is needed later when we come to add a toolbar. Secondly a weak property that refers back to the document. Thirdly, a UIToolbar.
In the code for the view controller, we firstly need to deal with the fact that the OUIDocumentViewController protocol declares a property called document. We shouldn’t synthesize it though, just implement these methods:
- (OUIDocument *)document { return self.locusDocument; } - (void)setDocument:(OUIDocument *)document { self.locusDocument = (LocusDocument*)document; }
All I’m doing there is assigning to and reading from the locusDocument property instead. The point of that is so that I can access it inside the class without having to cast all the time.
We also need to make sure the view controller uses the same undo manager as the document:
- (NSUndoManager *)undoManager; { return [self.locusDocument undoManager]; }
Now let’s make it display something:
- (void)loadView { [super loadView]; self.view.backgroundColor = [UIColor redColor]; }
Finally in your App Delegate, implement:
- (UIView *)pickerAnimationViewForTarget:(OUIDocument *)document; { return ((LocusDocumentViewController *)document.viewController).view; }
That should be enough to get something on the screen. At this point, I would expect that you can use the document picker’s Add button to create a new document, and that tapping that document will take you to a screen that is coloured in red.
Of course, from that screen you can’t get back again. For that you need:
Implementing the toolbar
Back in your App Delegate, declare the following instance variable:
@implementation AppDelegate { NSArray *_documentToolbarItems; }
Then add this method to the App Delegate, which is used to create a set of toolbar items:
- (NSArray *)toolbarItemsForDocument:(OUIDocument *)document; { if (!_documentToolbarItems) { NSMutableArray *items = [NSMutableArray array]; [items addObject:self.closeDocumentBarButtonItem]; [items addObject:self.undoBarButtonItem]; [items addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL] ]; UIBarButtonItem *omniPresenceBarButtonItem = [self.document omniPresenceBarButtonItem]; if (omniPresenceBarButtonItem != nil) [items addObject:omniPresenceBarButtonItem]; [items addObject:self.documentTitleToolbarItem]; [items addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL] ]; _documentToolbarItems = [[NSArray alloc] initWithArray:items]; } return _documentToolbarItems; }
Now we need to actually create and hook up a toolbar. We do this in the LocusDocumentViewController class:
- (void)loadView { [super loadView]; self.view.backgroundColor = [UIColor redColor]; self.toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 44.0)]; self.toolbar.autoresizesSubviews = YES; self.toolbar.barStyle = UIBarStyleBlackOpaque; self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; } - (void)viewDidLoad; { [super viewDidLoad]; OUIWithoutAnimating(^{ // Don't steal the toolbar items from any possibly open document if (!self.forPreviewGeneration) { self.toolbar.items = [[OUIDocumentAppController controller] toolbarItemsForDocument:self.document]; [self.toolbar layoutIfNeeded]; } }); } - (UIToolbar *)toolbarForMainViewController; { if (!self.toolbar) { [self view]; } return self.toolbar; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { [self _updateTitleBarButtonItemSizeUsingInterfaceOrientation:toInterfaceOrientation]; [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; } - (void)willMoveToParentViewController:(UIViewController *)parent; { if (parent) { [self _updateTitleBarButtonItemSizeUsingInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; } [super willMoveToParentViewController:parent]; } - (void)_updateTitleBarButtonItemSizeUsingInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; { AppDelegate *controller = [AppDelegate controller]; UIBarButtonItem *titleItem = [controller documentTitleToolbarItem]; UIView *customView = titleItem.customView; CGFloat newWidth = UIInterfaceOrientationIsPortrait(interfaceOrientation) ? 400 : 550; customView.frame = (CGRect){ .origin.x = customView.frame.origin.x, .origin.y = customView.frame.origin.y, .size.width = newWidth, .size.height = customView.frame.size.height }; }
These were also taken mostly from Omni’s sample code. All they’re doing is creating a toolbar when the view is loaded, and putting it into place. Whenever something like a rotation event happens, the toolbar is repositioned.
That’s all for now. You should be able to create new documents in the picker, view those documents (which just show up as a red screen), and return from viewing a document to the picker.
There’s a bug in here somewhere which means the document title isn’t centred in the toolbar. I’m not sure what’s happening there, so if anyone knows why, please let me know!