85: NSDocument using Uniform Type Identifier (UTI) and InfoPlist Example
Problem: we want to specify the filetypes an NSDocument based application is able to read and write and have it open and save such files.
Useful links:
Introduction to Uniform Type Identifiers Reference
Introduction to Uniform Type Identifiers Overview.
Also see the Wikipedia UTI entry.
The source to TextEdit makes useful reading. It may be found in /Developer/Examples/TextEdit.
To determine the Uniform Type Identifier (UTI) of a file use the mdls command in the Terminal.
Here are links to three more of my read and write examples:
033-NSDocument
034-NSDocument-NSTextView
035-Simple-Read-Write
Simplest NSDocument read and write
Use XCode to create a Cocoa Document based application. Call it DocApp.
The first thing to do is to see what the NSDocument read and write methods are able to tell us about the files they deal with.
Open MyDocument.m and modify the dataOfType and readFromData methods so they read as follows:
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { NSLog(@"dataOfType typeName=%@",typeName); return nil; } // end dataOfType - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"readFromData typeName=%@",typeName); return NO; } // end readFromData
Save, build and run. Use File->Open and try to open a file. Most filenames should be blanked out but you might find a file clicking on which causes the Open button to light up. If that happens then click Open. An error message panel will appear. Tell it to go away. In the console the type name should appear as DocumentType.
Now use File->Save or File->SaveAs. When the Save Panel appears click Save. The error message panel will appear. Tell it to go away and examine the console. Again the type name should appear as DocumentType.
Make the application write something to file. Within the dataOfType method that means converting whatever is to be saved into an object of type NSData.
Lets start with an NSString object. Convert it using the method - (NSData *)dataUsingEncoding:(NSStringEncoding)encoding and one of the NSString encoding types e.g. NSASCIIStringEncoding. Modify dataOfType so it reads as follows:
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { NSLog(@"dataOfType typeName=%@",typeName); NSString * zString = [NSString stringWithString:@"The Owl and the Pussycat went to sea"]; NSData * zData = [zString dataUsingEncoding:NSASCIIStringEncoding]; return zData; } // end dataOfType
Build and run. Do File->Save and save as the default: Untitled.????. Use XCode to open the file. It should contain the words The owl and the Pussycat went to sea.
Close the application window. Do File->Open or File->Open Recent and choose Untitled.????.
Again you will obtain the error panel. This is because we are returning NO from the readFromData method. Change that to YES and the error panel will no longer appear. The Console shows that the file was of the type DocumentType.
Modify the readFromData method so we can read in the file. Use the NSString method - (id)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding and the same encoding type as before, viz:NSASCIIStringEncoding.
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"readFromData typeName=%@",typeName); NSString * zString = [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding]; NSLog(@"readFromData zString=%@",zString); return YES; } // end readFromData
Build and run. Do File->Open The console should display the text The owl and the Pussycat went to sea.
Take a look at how Spotlight has indexed the file. Open a Finder window and navigate to the file Untitled.????. Open the Terminal. Type cd followed by space. Now from the Finder drag the folder containg the file Untitled.???? onto the Terminal window and let go. That will insert the path to the folder. Hit return and list the folder contents. At the prompt, type mdls Untitled.???? and hit return. Your Terminal window should contain text similar to that shown below.
Spotlight information about the file Untitled
There are two entries of special interest to us here. The first is that labelled kMDItemKind = "DocumentType" which is the string which is passed to the dataOfType and readFromData methods.
The second is that labelled kMDItemContentTypeTree which contains "public.data" and "public.item" both of which are Uniform Type Identifiers (UTI) listed in the Uniform Type Identifiers Reference. These are the values we will want to put into the application's InfoPlist to tell it which files we want to be able to open and save as.
To access the InfoPlist: in XCode, under Groups and Files click open the Targets group, select DocApp and click on the Info button. In the info panel select the Properties tab.
XCode properties for InfoPlist document types
Click the "+" button at the bottom left of the panel so as to insert a new entry.
Under Name put "Unknown document"
Under UTI put "public.data"
Set Class to MyDocument - this is the name of your NSDocument subclass in your project.
Set Store type to "Binary"
and Role to "Editor"
XCode Document Types
Build and run. Do File->open. There is not now a file the program appears unwilling to open. Try opening a few and see what happens.
It manages to read in quite a number of text based files as strings, e.g. XCode .h and .m files. Notice the various file types being listed.
Do File->SaveAs, (If you do File->Save you run the risk of overwriting the last file you read in!!!!). Notice that now the Save Panel shows two file types that the file can be saved as: "Unknown document" and "DocumentType"; the two entries in the Name collumn of the Info panel.
Using the methods: readFromURL and writeToURL
Comment out or delete the dataOfType method. Replace it by writeToURL as shown. This example uses a very simple write procedure which formed the subject of an interesting discussion on the CocoaBuilder mailing list.
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"writeToURL typeName=%@",typeName); NSString * zString = [NSString stringWithString:@"In a beautiful pea-green boat"]; BOOL zBoolResult = [zString writeToURL:absoluteURL atomically:YES encoding:NSASCIIStringEncoding error:outError]; return zBoolResult; } // end writeToURL
Now include some code to check the error status. Notice the double indirection of the passed error parameter.
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"writeToURL typeName=%@",typeName); NSString * zString = [NSString stringWithString:@"In a beautiful pea-green boat"]; BOOL zBoolResult = [zString writeToURL:absoluteURL atomically:YES encoding:NSASCIIStringEncoding error:outError]; if ([*outError code]) { NSLog(@"writeToURL outError code= %d",[*outError code]); NSLog(@"writeToURL outError domain= %@",[*outError domain]); NSLog(@"writeToURL outError localizedDescription= %@", [*outError localizedDescription]); return NO; } // end if return zBoolResult; } // end writeToURL
Here is the simple read.
- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"readFromURL typeName=%@",typeName); NSString *zString = [NSString stringWithContentsOfURL:absoluteURL encoding:NSUTF8StringEncoding error:outError]; if ([*outError code]) { NSLog(@"readFromURL outError code= %d",[*outError code]); NSLog(@"readFromURL outError domain= %@",[*outError domain]); NSLog(@"readFromURL outError localizedDescription= %@", [*outError localizedDescription]); return NO; } // end if NSLog(@"readFromURL zString=%@",zString); return YES; } // end readFromURL
Using the methods: readFromFileWrapper and fileWrapperOfType
We will end up by creating two NSFileWrapper file objects and wrapping these in a dictionary NSFileWrapper which is then written to disk. Start by making a simple NSFileWrapper object and writing it to file and then reading it back.
- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError { NSLog(@"fileWrapperOfType typeName=%@",typeName); NSString * zString = [NSString stringWithString: @"The Owl and the Pussycat went to sea"]; NSData * zDataOwlAndPussycat = [zString dataUsingEncoding:NSASCIIStringEncoding]; NSFileWrapper * zFileWrapperOwl = [[NSFileWrapper alloc]initRegularFileWithContents:zDataOwlAndPussycat]; return zFileWrapperOwl; } // end fileWrapperOfType - (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"readFromFileWrapper typeName=%@",typeName); if ([fileWrapper isRegularFile]) { NSLog(@"readFromFileWrapper isRegularFile"); NSData * zData = [fileWrapper regularFileContents]; NSString *zString = [[NSString alloc]initWithData:zData encoding:NSASCIIStringEncoding]; NSLog(@"readFromFileWrapper zString=%@",zString); return YES; } // end if if ([fileWrapper isDirectory]) { NSLog(@"readFromFileWrapper isDirectory"); } // end if return YES; } // end readFromFileWrapper
Build and run. Do File->Save As. You can examine the contents of the file by opening it in XCode. Close the current window and use File->Open to open the saved file. It's contents will be displayed in the Console.
Lets see if we can create a directory wrapper. Update fileWrapperOfType so it looks as follows.
- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError { NSLog(@"fileWrapperOfType typeName=%@",typeName); NSString * zString = [NSString stringWithString: @"The Owl and the Pussycat went to sea"]; NSData * zDataOwlAndPussycat = [zString dataUsingEncoding:NSASCIIStringEncoding]; NSFileWrapper * zFileWrapperOwl = [[NSFileWrapper alloc]initRegularFileWithContents:zDataOwlAndPussycat]; zString = [NSString stringWithString:@"In a beautiful pea-green boat"]; NSData * zDataPeaGreenBoat = [zString dataUsingEncoding:NSASCIIStringEncoding]; NSFileWrapper * zFileWrapperBoat = [[NSFileWrapper alloc]initRegularFileWithContents:zDataPeaGreenBoat]; NSDictionary * zDictOfFileWrappers = [ NSDictionary dictionaryWithObjectsAndKeys: zFileWrapperOwl,@"FileOwl", zFileWrapperBoat,@"FileBoat", nil]; NSFileWrapper * zFileWrapperDirectory = [[NSFileWrapper alloc]initDirectoryWithFileWrappers:zDictOfFileWrappers]; return zFileWrapperDirectory; //return zFileWrapperBoat; } // end fileWrapperOfType
Build and run. Do File->Save As and save as myWrappedFileDirectory say. The code produces the directory and within it two files: FileOwl and FileBoat. Brilliant. I had thought though that it was going to create something more akin to a bundle!
Lets see what happens if we try to open that directory. Update readFromFileWrapper so it looks as follows.
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError { NSLog(@"readFromFileWrapper typeName=%@",typeName); if ([fileWrapper isRegularFile]) { NSLog(@"readFromFileWrapper isRegularFile"); NSData * zData = [fileWrapper regularFileContents]; NSString *zString = [[NSString alloc]initWithData:zData encoding:NSASCIIStringEncoding]; NSLog(@"readFromFileWrapper zString=%@",zString); return YES; } // end if if ([fileWrapper isDirectory]) { NSLog(@"readFromFileWrapper isDirectory"); NSFileWrapper *zFileWrapperChild; [fileWrapper removeFileWrapper:zFileWrapperChild]; NSData * zData = [fileWrapper regularFileContents]; NSString *zString = [[NSString alloc]initWithData:zData encoding:NSASCIIStringEncoding]; NSLog(@"readFromFileWrapper 1st child zString=%@",zString); return YES; } // end if return YES; } // end readFromFileWrapper
Build and run. When we try to read myWrappedFileDirectory though we get an error panel which states: "The document 'myWrappedFile' could not be opened. DocApp cannot open files in the 'folder' format.".
But maybe we can open bundles? That requires the correct UTI to be defined in the InfoPlist.
Use the mdls command in Terminal to discover the UTI for an application.
Now add another line to the Document Types list in the Target's Info Property panel.
Set name to "Application Bundle",
set UTI to "com.apple.application-bundle",
Class to "Binary",
Store Type to "com.apple.application-bundle",
and Role to "Editor".
Build and run. Find an application and open it. It worked for me even though the console said it had ignored an exception.
Download
This XCode 3.2.2 project exists as a 2.3 MB download.