Ad

Our DNA is written in Swift
Jump

Back on Mac – OS X Tutorial for iOS Developers

I’ve been programming for the iOS platform ever since this is possible, since the iPhone 3G with iPhone OS 2.0 was released by Apple in Summer 2008. For all this time I had a healthy respect about programming for Mac. More precisely: Horror.

If you dig into it you can only applaud Apple for not having tried to craft touch screen and energy optimization stuff onto AppKit, but chose to go the forked OS route. Being a seasoned iOS developer you will find yourself often cursing about how complicated certain activities seem.

Having said that you also see the positive influence of iOS on AppKit all around. Now that Apple deprecated Garbage Collection and you are already well used to programming under the ARC paradigm you find yourself writing exactly the same code for both platforms more often than not.

This will be the first in a series of tutorials where I am sharing my experiences in diving into AppKit. Please let me know if this is in fact interesting to you by sharing and Flattr’ing it.

Rather than showing you messy code I am working on right now I decided to start this first part of the tutorial from scratch.

NSDocument-based Photo Shoebox for Mac

When we’re done with this tutorial we will have a document-based Mac app that has the following features:

  • Out-of-the-Box Document features: New Document, Rename, Move, Lock, Revert to earlier versions
  • Multi-Document-Interface, you can have as many documents open as you like
  • Custom icon for those Shoebox bundles, double-clicking will open the document
  • A document will be a collection of images, displayed in a grid
  • You can drag images from your desktop into a document and it will be added to the document
  • Dragging an image out of a document creates a copy of it on the desktop
  • You can rearrange the images also by dragging them around on the grid

Source code will be made available in my Examples GitHub repo.

NSDocument versus UIDocument

Document-based apps on Mac and iOS have something in common: there is a base-class from which you derive your own document class. This class is NSDocument on Mac and UIDocument on iOS. The reason for this split is because NSDocument fits in the Controller category of the Model-View-Controller paradigm. As such it is responsible for creating the Window that represents the document. Apple couldn’t have reused the same class for iOS (it was first on the Mac) because then it would have had a problem with all the Mac-specific controller methods. Because of this you have the iOS pendant in the form of UIDocument.

UIDocument (new in iOS 5) and NSDocument (around since OS X 10.0) both can be enabled to support iCloud. You’ll notice that Apple removed the display-controllery bits from NSDocument for UIDocument and left it entirely document-controlling on iOS. On Mac you’ll find methods grouped under “Creating and Managing Window Controllers” and “Managing Document Windows” which are totally absent from UIDocument. I suspect that this is mainly because Apple is now more strictly separating the MVC parts. NSDocument would totally fail this new approach as it mixes UI and document-interaction.

There are four kinds of document types that come to mind:

  • Flat file – use the data reading/writing methods of NSDocument
  • Bundle – use the fileWrapper reading/writing methods of NSDocument
  • CoreData – use NSPersistentDocument  (Mac only) which adds CoreData stuff to NSDocument
  • Custom – use the direct URL-based reading/writing methods of NSDocument

Note that bundles are a Mac speciality where a folder is made to be looking like a single file. Bundles are great while moving on Apple’s eco-system, but if you’re moving outside of that you’ll probably want to support a single-file format instead, or zip the bundle up to make it a single file for transmission.

NSFileWrapper

Our tutorial app will be using bundles. Each document will be a folder with a .shoebox extension and this will contain any number of assorted image files. A property list (plist) will keep track of our sort order. Because of this we will be using the approach via NSFileWrapper. This ancient (since OS X 10.0) class is available also on iOS (since iOS 4).

File wrappers are object-representations of single files, folders or symbolic links. You can think of them as a wrapper around a file system node that more or less exists independently from a physical location. It can and does happen all the time that a document will be moved somewhere else or be renamed without you being informed to that effect. But even if that occurs your file wrappers still let you access the file data. You don’t have to care where the actual files are located.

We won’t deal with symbolic links in this tutorial. Only files and folders. You can query a file wrapper isRegularFile, isDirectory or isSymbolicLink to quickly tell them apart. If you are talking to a folder then the file wrapper has its children in a dictionary named fileWrappers. Each dictionary key is the name of the child and each referenced value is in turn another NSFileWrapper. To get to the contents of an individual file you call regularFileContents to retrieve and NSData instance.

File – New -Project

Armed with these basic principles we can get started with actual code. We’ll start a new Mac Cocoa app.

We don’t check “Use Core Data” because that would cause the template to set up the app using NSPersistentDocument. Also we already specify our document extension here. This sets up a document type in the info of the app target.

Let’s dive into this and customize it a bit. Take note of the Class “Document” which maps this type to our Document.h/.m/.xib. We change the Name to “Photo Shoebox”, set an identifier “com.drobnik.shoebox”, note that the role of our app will be to be an “Editor” of this type. Also we want to check the “Document is distributed as a bundle” option.

This icon can be any icns file that is located in the app bundle. More on that later.

If you run the app in this current state you will notice a warning logged on the console:

-[NSDocumentController fileExtensionsFromType:] is deprecated, and does not work when passed a uniform type identifier (UTI). If the application didn’t invoke it directly then the problem is probably that some other NSDocument or NSDocumentController method is getting confused by a UTI that’s not actually declared anywhere. Maybe it should be declared in the UTExportedTypeDeclarations section of this app’s Info.plist but is not. The alleged UTI in question is “com.drobnik.shoebox”.

So let’s also take care of the “Maybe it should be declared” right away. I yet have to find anybody who can explain the link between document types exported and imported UTIs. I know this from my own experiments. This setup registers our UTI so that the rest of the operating system knows that we are responsible for it.

With this registration in place the above mentioned warning does no longer occur. If anybody can shed some light on how this works, please contact me.

Upon inspection of Document.m you see that the template put in dataOfType:error: and readFromData:ofType:error: including the immediate throwing of an NSException should you not customize these. The Document.xib contains a Window that has one View plus some static text “Your document contents here”. We’ll be customizing those files in a bit, but before we need a bit of model.

Model

Since we want to be able to have our own order independent from a sort order by name we are going to keep the model objects in a mutable array. Because we also want to display the file name for each file we have to give our model object a name property too. To make it a bit more interesting we’re not going the easy route and also give the model object an image property. Instead we supply a weak link to the document from where each model object can retrieve the image. This is probably also a bit more efficient because as far as I can tell file wrappers only access the information on disk when they really needed it.

DocumentItem.h

@class Document;
 
@interface DocumentItem : NSObject
 
@property (nonatomic, strong) NSString *fileName;
@property (nonatomic, weak) Document *document;
 
- (NSImage *)thumbnailImage;
 
@end

DocumentItem.m

#import "DocumentItem.h"
#import "Document.h"
 
@implementation DocumentItem
 
- (NSImage *)thumbnailImage
{
	return [self.document thumbnailImageForName:self.fileName];
}
 
@synthesize fileName;
@synthesize document;
 
@end

The document property is weak because document items will be owned by the property and having a strong reference would cause a retain cycle. Weak properties have the added benefit that should a DocumentItem ever survive its Document then the deallocation of the document will automatically set the property to nil preventing a crash if some delayed method is still making a method call to it.

We’ll be using an NSKeyedArchiver to persist our array of DocumentItems, so we also need to implement the NSCoding protocol methods for our model object. Of course we could have also done this in CoreData but for simplicity’s sake we’ll still with keyed archiving.

#pragma mark NSCoding
 
- (id)initWithCoder:(NSCoder *)aDecoder
{
	self = [super init];
 
	if (self)
	{
		self.fileName = [aDecoder decodeObjectForKey:@"fileName"];
	}
 
	return self;
}
 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
	[aCoder encodeObject:self.fileName forKey:@"fileName"];
}

Next up we need to teach our Document how to encode and decode an array of DocumentItems with file wrappers.

Implement Reading

At this stage we won’t have shoebox bundles yet to test it, so just take my word for it that this works. Typically you will overwrite one of these three methods:

  • – (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError
  • – (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
  • – (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError;

Can you guess which one is the one we want? Hint: see “four kinds of document types”.

Document.h

@interface Document : NSDocument
 
@property (nonatomic, strong) NSMutableArray *items;
 
- (NSImage *)thumbnailImageForName:(NSString *)fileName;
 
@end

Document.m

#import "Document.h"
#import "DocumentItem.h"
 
@implementation Document
{
	NSFileWrapper *_fileWrapper;
	NSMutableArray *_items;
}
 
- (id)init
{
    self = [super init];
    if (self)
    {
        // start out with an empty array
        _items = [[NSMutableArray alloc] init];
 
        // start with an empty file wrapper
        _fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
    }
    return self;
}
 
- (NSString *)windowNibName
{
	return @"Document";
}
 
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
{
	[super windowControllerDidLoadNib:aController];
}
 
+ (BOOL)autosavesInPlace
{
    return YES;
}
 
// below this line our own code starts ---
 
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError
{
	if (![fileWrapper isDirectory])
	{
		NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"Illegal Document Format" forKey:NSLocalizedDescriptionKey];
		*outError = [NSError errorWithDomain:@"Shoebox" code:1 userInfo:userInfo];
		return NO;
	}
 
	// store reference for later image access
	_fileWrapper = fileWrapper;
 
	// decode the index
	NSFileWrapper *indexWrapper = [fileWrapper.fileWrappers objectForKey:@"index.plist"];
	NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithData:indexWrapper.regularFileContents];
 
	// set document property for all items
	[array makeObjectsPerformSelector:@selector(setDocument:) withObject:self];
 
	// set the property
	self.items = [array mutableCopy];
 
	// YES because of successful reading
	return YES;
}
 
@synthesize items = _items;
 
@end

Regardless of what you implement, the first called method will be readFromURL and the default implementation in NSDocument decides whether to call the file wrapper method or the data reading method. If you wanted to you could grab the document file URL from there, but I strongly advice against that. If the user chooses to move or rename the document while it is open in a window the URL will become invalid. There are several scenarios where the document might end up in a different spot on your hard disk, including renaming, duplicating, auto-saving and moving of the document.

Instead of having to worry about hard disk locations we’re holding onto the file wrapper that is passed to us. This allows us to access document resources (i.e. the images) later on. So this code is just a quick validity check to refuse flat files that might have ended up with a .shoebox extension.

Implement Writing

Similar to the reading you also have a choice of three methods, one of which you must implement.

  • – (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError
  • – (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
  • – (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError

Here we have to do a little bit more besides of encoding the item array. We also need to make sure that the new file wrapper contains all the images from the original bundle. So we walk through the item list, get the corresponding file wrappers for the images by name and add them to the list of files to be added.

- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError
{
	// this holds the files to be added
	NSMutableDictionary *files = [NSMutableDictionary dictionary];
 
	// encode the index
	NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.items];
	NSFileWrapper *indexWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:data];
 
	// add it to the files
	[files setObject:indexWrapper forKey:@"index.plist"];
 
	// copy all other referenced files too
	for (DocumentItem *oneItem in self.items)
	{
		NSString *fileName = oneItem.fileName;
		NSFileWrapper *existingFile = [_fileWrapper.fileWrappers objectForKey:fileName];
		[files setObject:existingFile forKey:fileName];
	}
 
	// create a new fileWrapper for the bundle
	NSFileWrapper *newWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:files];
 
	return newWrapper;
}

That’s enough meat for part 1. Let’s Build&Run and see what already works.

Result!!!

At this stage we have all the code necessary to get the out-of-the-box features that NSDocument gives us. Build&Run the app and you find that you can create a new document via the File Menu.

Also Open, Open Recent, Close, Save, Duplicate, Rename, Move To and Reverting work already! Only the Print lacks an implementation at this point. Also the handling of multiple open documents is complete. You can minimize document windows and switch between them.

If you create a new document then it is created in a special auto-save location under the user’s Library folder. Our Shoebox documents show up as files, and you have to “Show Package Contents” to peek inside. There you will find an index.plist file that is a keyed archive.

I’m sorry that we don’t have anything more visible yet, but I promise that we’ll get to drag/drop and NSCollectionView next. We’re done with the boring part and I promise that it will soon become more exciting as we enable adding and removing of images to our Shoebox documents.

Also, please let me know if you find this kind of iOS-to-Mac crossover tutorial useful. Use the Flattr button and/or share this article on your favorite social networks so that I can see if it is worth to spend the time on putting this tutorial together and seeing it through to the end.

On to Part 2.


Categories: Recipes

2 Comments »

  1. Regarding the exported imported UTI, apple explain it at the start of this https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_declare/understand_utis_declare.html.

    Basically import UTIs are for files you want to read, and export are for ones you want to write.

    You get that error because you tried to write or prepare to write a file for a UTI you didnt declare.

Trackbacks

  1. OS X Tutorial for iOS Developers | Touch Up InsideTouch Up Inside