iTunes Tutorial for iOS: How To Integrate iTunes File Sharing With Your iOS App

An iTunes tutorial on how to let users easily copy files to and from your app using the iTunes File Sharing feature. By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Importing and Exporting Our Documents

Ok enough talk, time for action! First things first – we’re going to use some helper code from the users at CocoaDev.com for gzip/gunzip. So grab a copy, and drag the two files into the Helpers group of your project.

Also, while you’re at it, these files require zlib, so go to your project settings and add “-lz” to your “Other Linker Flags”.

Now onto the code! Let’s start by adding the code to our ScaryBugDoc to support exporting and importing our documents as a zipped file that we can share with File Sharing.

First make the following mods to ScaryBugDoc.h:

// After @interface
- (NSString *)getExportFileName;
- (NSData *)exportToNSData;
- (BOOL)exportToDiskWithForce:(BOOL)force;
- (BOOL)importFromPath:(NSString *)importPath;

Just declaring some methods we’re about to implement here. Switch over to ScaryBugDoc.m and let’s implement them one by one:

1) Implement getExportFileName

// Add to top of file
#import "NSData+CocoaDevUsersAdditions.h"

// Add new method
- (NSString *)getExportFileName {
    NSString *fileName = _data.title;
    NSString *zippedName = [fileName stringByAppendingString:@".sbz"];
    return zippedName;
}

This method will return the name that we’re going to export our bug as. We don’t want to export the bug with a simple numeric directory name like our documents are named internally, because then our user will have no good way of knowing what’s inside. So instead, we use the title of the bug to construct the filename.

Speaking of which, I’m not sure if there’s an easy way to massage the filename so it doesn’t contain any unsupported characters on both Mac and Windows, anybody know a solution to that?

The other thing to note is that we end the filename with an extension “.sbz” which we’ll register our app as being able to open later. When I was first playing around with this, I tried using a double extension such as “.scarybug.gz”, but when I was trying to open the attachment from Mail, it would never launch my app, and I suspect it didn’t like the double extension. So I recommend using just a single extension for now.

2) Implement exportToNSData

So now we need a method to take our directory and convert it into a single buffer of NSData so we can write it out to a single file.

There are different ways to do this – one popular way is to zip the directory up using the open source ZipArchive library. Another popular way is to use a combination of tar and gzip code. But I thought I’d show you another way: using NSFileWrapper to serialize the data, then gzipping that up.

- (NSData *)exportToNSData {
    NSError *error;
    NSURL *url = [NSURL fileURLWithPath:_docPath];
    NSFileWrapper *dirWrapper = [[[NSFileWrapper alloc] initWithURL:url options:0 error:&error] autorelease];
    if (dirWrapper == nil) {
        NSLog(@"Error creating directory wrapper: %@", error.localizedDescription);
        return nil;
    }   
    
    NSData *dirData = [dirWrapper serializedRepresentation];
    NSData *gzData = [dirData gzipDeflate];    
    return gzData;
}

NSFileWrapper is a new class available in iOS4+ that among other things provides an easy way to serialize entire directory contents. As you can see it’s pretty simple to use here: we just initialize it with a URL, then we can get an NSData representation of a directory by calling serializedRepresentation.

This isn’t compressed, so we use the gzipDeflate helper method from the NSData extensions we downloaded earlier to do that.

3) Implement exportToDiskWithForce

- (BOOL)exportToDiskWithForce:(BOOL)force {
 
    [self createDataPath];
      
    // Figure out destination name (in public docs dir)
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];      
    NSString *zippedName = [self getExportFileName];
    NSString *zippedPath = [documentsDirectory stringByAppendingPathComponent:zippedName];
    
    // Check if file already exists (unless we force the write)
    if (!force && [[NSFileManager defaultManager] fileExistsAtPath:zippedPath]) {
        return FALSE;
    }
    
    // Export to data buffer
    NSData *gzData = [self exportToNSData];
    if (gzData == nil) return FALSE;
    
    // Write to disk
    [gzData writeToFile:zippedPath atomically:YES];
    return TRUE;
    
}

The first thing we do here is construct the full path to where we’re going to save our zipped document. Note this time we’re saving in the Documents directory (not Library\Private Data), because the Documents directory is what’s available for File Sharing.

Next we check to see if the file is already there. If it is, we’re going to want to present a warning to the user, so we return FALSE unless the user forces the save.

Finally, we just make a call to export it as NSData, and simply write it out to the disk.

4) Implement importFromPath

- (BOOL)importData:(NSData *)zippedData {
    
    // Read data into a dir Wrapper
    NSData *unzippedData = [zippedData gzipInflate];                
    NSFileWrapper *dirWrapper = [[[NSFileWrapper alloc] initWithSerializedRepresentation:unzippedData] autorelease];
    if (dirWrapper == nil) {
        NSLog(@"Error creating dir wrapper from unzipped data");
        return FALSE;
    }
    
    // Calculate desired name
    NSString *dirPath = [ScaryBugDatabase nextScaryBugDocPath];                                
    NSURL *dirUrl = [NSURL fileURLWithPath:dirPath];                
    NSError *error;
    BOOL success = [dirWrapper writeToURL:dirUrl options:NSFileWrapperWritingAtomic originalContentsURL:nil error:&error];
    if (!success) {
        NSLog(@"Error importing file: %@", error.localizedDescription);
        return FALSE;
    }
        
    // Success!
    self.docPath = dirPath;
    return TRUE;    
    
}

- (BOOL)importFromPath:(NSString *)importPath {
    
    // Read data into a dir Wrapper
    NSData *zippedData = [NSData dataWithContentsOfFile:importPath];
    return [self importData:zippedData];
    
}

We’re actually going to extract most of the work from importFromPath into a helper function called importData, because it will be useful later.

In importData, we just do the opposite of what we did in exportData – we inflate the zipped contents and use NSFileWrapper to expand it again with writeToURL:options:originalContentsURL:error.

As for the destination file name, we just create the next available file name. So we’re never overwriting an existing file when you import, we always create a new file. This is by design to avoid the user from accidentally overwriting their files. If they import the same file twice, they’ll have a duplicate, but they can easily delete files.

Ok – that’s it for the core code. There’s still a bit more to do to integrate into the rest of the app though – we have to make some mods to the ScaryBugDatabase and add some GUI elements to support this.

Integration into App

Just a few steps to integrate this into the rest of the app…

1) Add a helper function to ScaryBugDatabase

We’re going to need a method to return the list of documents that the user can choose to import, so add the following to ScaryBugDatabase.h:

+ (NSMutableArray *)importableScaryBugDocs;

Then implement it in ScaryBugDatabase.m:

+ (NSMutableArray *)importableScaryBugDocs {
    
    NSMutableArray *retval = [NSMutableArray array];
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *publicDocumentsDir = [paths objectAtIndex:0];   
    
    NSError *error;
    NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:publicDocumentsDir error:&error];
    if (files == nil) {
        NSLog(@"Error reading contents of documents directory: %@", [error localizedDescription]);
        return retval;
    }
    
    for (NSString *file in files) {
        if ([file.pathExtension compare:@"sbz" options:NSCaseInsensitiveSearch] == NSOrderedSame) {        
            NSString *fullPath = [publicDocumentsDir stringByAppendingPathComponent:file];
            [retval addObject:fullPath];
        }
    }
    
    return retval;
    
}

This should be fairly straightforward stuff by now – we just enumerate all of the files in the Documents directory, looking for anything that ends with sbz, and add the full path of anything we find to a NSMutableArray.

2) Modify EditBugViewController to export documents

Make the following changes to EditBugViewController.h:

// Add the UIAlertViewDelegate protocol to the interface declaration
@interface EditBugViewController : UIViewController <UITextFieldDelegate, RateViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAlertViewDelegate> {

Then make the following changes to EditBugViewController.m:

// Add inside viewDidLoad
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Export" style:UIBarButtonItemStyleBordered target:self action:@selector(exportTapped:)] autorelease];

// Add new function
- (void)exportTapped:(id)sender {
    [DSBezelActivityView newActivityViewForView:self.navigationController.navigationBar.superview withLabel:@"Exporting Bug..." width:160];   
    [_queue addOperationWithBlock: ^{
        BOOL exported = [_bugDoc exportToDiskWithForce:FALSE];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [DSBezelActivityView removeViewAnimated:YES];
            if (!exported) {
                UIAlertView *alertView = [[[UIAlertView alloc] 
                                           initWithTitle:@"File Already Exists!" 
                                           message:@"An exported bug with this name already exists.  Overwrite?" 
                                           delegate:self 
                                           cancelButtonTitle:@"Cancel" 
                                           otherButtonTitles:@"Overwrite", nil] autorelease];
                [alertView show];
            }
        }];
    }]; 
}

- (void) alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
 
    if (buttonIndex == alertView.firstOtherButtonIndex + 0) {
        [DSBezelActivityView newActivityViewForView:self.navigationController.navigationBar.superview withLabel:@"Exporting Bug..." width:160];   
        [_queue addOperationWithBlock: ^{
            [_bugDoc exportToDiskWithForce:TRUE];
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                [DSBezelActivityView removeViewAnimated:YES];
            }];
        }]; 
    }
    
}

Here we just add a new navigation item to our bar with the title “Export”. When the user taps it, we’ll try exporting to disk – and if the file exists present them with a warning, and only overwrite it if they accept.

Note we’re using NSOperationQueues and an activity indicator to make for a nicer user experience for the user. If you’re unsure about how this works, check out the How To Create A Simple iPhone App Tutorial: Part 3/3 tutorial.

3) Create a new view controller to allow user to choose a document to import

We’re going to need a new view controller to list the importable documents so the user can choose one.

We’ll just do something quick and dirty for this. Right click on the View Controllers group, and click “Add\New File…”. Choose UIViewController subclass, and make sure “Targeted for iPad” and “With XIB for user interface” are unchecked, but “UITableViewController subclass” IS checked, and click Next. Name the file ImportBugViewController.m, and click Finish.

Then replace ImportBugViewController.h with the following:

#import <UIKit/UIKit.h>

@protocol ImportBugViewControllerDelegate
- (void)importableBugDocTapped:(NSString*)importPath;
@end


@interface ImportBugViewController : UITableViewController {
    NSMutableArray *_importableBugDocs;
    id <ImportBugViewControllerDelegate> _delegate;
}

@property (copy) NSMutableArray *importableBugDocs;
@property (assign) id <ImportBugViewControllerDelegate> delegate;

@end

Note we’re setting up a delegate here so we can notify the root view controller when the user selects a bug to import.

Next make the following changes to ImportBugViewController.m:

// Under @implementation
@synthesize importableBugDocs = _importableBugDocs;
@synthesize delegate = _delegate;

// Uncomment viewDidLoad and add the following inside
self.title = @"Import Bug";

// Uncomment shouldAutorotateToInterfaceOrientation and set the return value to:
return YES;

// Set the return value of numberOfSectionsInTableView to this:
return 1;

// Set the return value of tableView:numberOfRowsInSection to this:
return _importableBugDocs.count;

// In tableView:cellForRowAtIndexPath, add the following after "Configure the cell":
NSString *fullPath = [_importableBugDocs objectAtIndex:indexPath.row];
NSString *fileName = [fullPath lastPathComponent];
cell.textLabel.text = fileName;

// Replace the contents of tableView:didSelectRowAtIndexPath with the following:
NSString *fullPath = [_importableBugDocs objectAtIndex:indexPath.row];
[_delegate importableBugDocTapped:fullPath];

// Add the following to dealloc
[_importableBugDocs release];
_importableBugDocs = nil;
_delegate = nil;

This should all be pretty standard table view setup to you by now, so no need to discuss this further.

One thing to note though: here we’re displaying just the filename of the object, because to get anything else we’d have to unzip the documents (an expensive operation). However Pages seems to have a thumbnail to go along with their documents. Anyone have any idea how they managed that?

4) Modify RootViewController to have an Import from File Sharing Option

Make the following mods to RootViewController.h:

// At top of file
#import "ImportBugViewController.h"
#import "ScaryBugDatabase.h"

// Modify @interface to incldue the UIActionSheetDelegate and ImportBugViewControllerDelegate
@interface RootViewController : UITableViewController <UIActionSheetDelegate, ImportBugViewControllerDelegate> {

// Add inside @interface
ImportBugViewController *_importBugViewController;

// After @interface
@property (retain) ImportBugViewController *importBugViewController;

Next, make the following changes to RootViewController.m:

// Under @implementation
@synthesize importBugViewController = _importBugViewController;

// In didReceiveMemoryWarning
self.importBugViewController = nil;

// In dealloc
[_importBugViewController release];
_importBugViewController = nil;

// Replace addTapped with this:
- (void)addTapped:(id)sender {
 
    UIActionSheet *actionSheet = [[[UIActionSheet alloc]
                                   initWithTitle:@"" 
                                   delegate:self 
                                   cancelButtonTitle:@"Cancel" 
                                   destructiveButtonTitle:nil 
                                   otherButtonTitles:@"Add New Bug", @"Import Bug", nil] autorelease];
    [actionSheet showInView:self.view];
                
}

// Add new functions
- (void)addNewDoc:(ScaryBugDoc *)newDoc {
    [_bugs addObject:newDoc];        
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_bugs.count-1 inSection:0];
    NSArray *indexPaths = [NSArray arrayWithObject:indexPath];    
    [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
    
    if (buttonIndex == actionSheet.firstOtherButtonIndex + 0) {
        
        ScaryBugDoc *newDoc = [[[ScaryBugDoc alloc] initWithTitle:@"New Bug" rating:0 thumbImage:nil fullImage:nil] autorelease];
        [newDoc saveData];
        
        [self addNewDoc:newDoc];
        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_bugs.count-1 inSection:0];
        [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
        [self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
        
    } else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1) {
                
        if (_importBugViewController == nil) {
            self.importBugViewController = [[[ImportBugViewController alloc] initWithStyle:UITableViewStylePlain] autorelease];
            _importBugViewController.delegate = self;
        }
        _importBugViewController.importableBugDocs = [ScaryBugDatabase importableScaryBugDocs];
        [self.navigationController pushViewController:_importBugViewController animated:YES];
        
    }
    
}

- (void)importableBugDocTapped:(NSString*)importPath {
    [self.navigationController popViewControllerAnimated:YES];
    ScaryBugDoc *newDoc = [[[ScaryBugDoc alloc] init] autorelease];
    if ([newDoc importFromPath:importPath]) {
        [self addNewDoc:newDoc];
    }
}

So, what we did here was make it so when the user taps the “+” button, instead of just adding a row we ask the user if they want to add a row, or import an existing doc.

If they choose to import a doc, we create our import bug view controller, get the list of importable docs from the ScaryBugDatabase, and pass that onto the controller and dispaly it.

Finally when the user chooses a doc, we call the importFromPath method we wrote earlier, and add the document.

One last thing then we’re done!

5) Enable File Sharing in the Info.plist

Finally open up ScaryBugs-Info.plist and add a new boolean key called UIFileSharingEnabled and make sure it’s checked:

UIFileSharingEnabled

Phew! It was a long, crazy process but we’re finally done and get to enjoy the fruits of our labor!