In-App Purchase content downloads in iOS6

26 Oct 2012

With iOS6, Apple added support for downloadable content to in-app purchases (IAP). This is a fantastic feature: It’s simpler (and probably more secure than rolling it yourself) and it’s more robust as it allows for downloads to occur in the background.

I’ve recently implemented downloadable content and it works great – the only real problem I ran into is that the documentation is sparse. By filling in some of the details here, I hope save others some time.

I’m assuming that you have some experience with in-app purchases (IAP). Also, I’d recommend you check out a talk from WWDC 2012 titled, Selling Products with StoreKit (If you don’t want to watch the whole video, the PDF of the slides is a good overview.)

This is what a standard StoreKit observer would look like:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray*)transactions;
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
            case SKPaymentTransactionStateRestored:
            {
                // NOT SHOWN: unlock features, start a download
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
        	}
            case SKPaymentTransactionStateFailed:
            {
                // NOT SHOWN: tell the user about the failure (could just be a cancel)
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
        	}
            default:
                break;

        }
    }	
}

To handle downloadable content, you simply check if the purchase has downloads, and if it does, you start them. It’s also important that you don’t finish the transaction until the download is complete.

case SKPaymentTransactionStatePurchased:
            case SKPaymentTransactionStateRestored:
            {
                if (transaction.downloads) {
                    [[SKPaymentQueue defaultQueue] startDownloads:transaction.downloads];
                } else {
                    // unlock features
                    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                }
                break;
        	}

How do we know when the download is complete? There is a new delegate method in SKPaymentTransactionObserver that is periodically called as the purchase downloads. You should update your model (and your UI) with the download progress, and process the download once it’s complete.

- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads;
{
    for (SKDownload *download in downloads) {

        if (download.downloadState == SKDownloadStateFinished) {            
            [self processDownload:download]; // not written yet
            // now we're done
            [queue finishTransaction:download.transaction];
            
        } else if (download.downloadState == SKDownloadStateActive) {
            
            NSString *productID = download.contentIdentifier; // in app purchase identifier
            NSTimeInterval remaining = download.timeRemaining; // secs
            float progress = download.progress; // 0.0 -> 1.0
            
            // NOT SHOWN: use the productID to notify your model of download progress...
                        
        } else {    // waiting, paused, failed, cancelled
            NSLog(@"Warn: not handled: %d", download.downloadState);
        }
    }
}

To process the download, you are given a contentURL which points to a directory with the contents of the download, so you just need to move them out of the temporary cache directory to a directory where they can be used by your app.

- (void) processDownload:(SKDownload*)download;
{
    // convert url to string, suitable for NSFileManager
    NSString *path = [download.contentURL path];

    // files are in Contents directory
    path = [path stringByAppendingPathComponent:@"Contents"];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    NSArray *files = [fileManager contentsOfDirectoryAtPath:path error:&error];
    NSString *dir = [MyConfig downloadableContentPathForProductId:download.contentIdentifier]; // not written yet

    for (NSString *file in files) {
        NSString *fullPathSrc = [path stringByAppendingPathComponent:file];
        NSString *fullPathDst = [dir stringByAppendingPathComponent:file];
    
        // not allowed to overwrite files - remove destination file
        [fileManager removeItemAtPath:fullPathDst error:NULL];
    
        if ([fileManager moveItemAtPath:fullPathSrc toPath:fullPathDst error:&error] == NO) {
            NSLog(@"Error: unable to move item: %@", error);
        }
    }

    // NOT SHOWN: use download.contentIdentifier to tell your model that we've been downloaded
}

The one piece we need to write is the +[MyConfig downloadableContentPathForProductId:]. This is a class method as it will be also used by the app to locate the downloaded files. Apple now requires that any content that is not user-created, should not be stored in the Documents directory. So, you need to use Library/Application Support. And to prevent user’s from consuming all of their iCloud quota with your downloads, you need to flag that directory so it will be ignored by the iCloud backup. (See: Tech QA 1719)

+ (NSString *) downloadableContentPath;
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
    NSString *directory = [paths objectAtIndex:0];
    directory = [directory stringByAppendingPathComponent:@"Downloads"];

    NSFileManager *fileManager = [NSFileManager defaultManager];

    if ([fileManager fileExistsAtPath:directory] == NO) {

        NSError *error;
        if ([fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error] == NO) {
            NSLog(@"Error: Unable to create directory: %@", error);
        }
        
        NSURL *url = [NSURL fileURLWithPath:directory];
        // exclude downloads from iCloud backup
        if ([url setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error] == NO) {
            NSLog(@"Error: Unable to exclude directory from backup: %@", error);
        }
    }
    
    return directory;
}

Preparing IAP Content

To create your IAP download, create a new project in Xcode, and then select the “IAP Purchase Content” template. Add whatever files you need, and make sure that the IAPProductIdentifier in the ContentInfo.plist matches the one you setup in iTunes Connect.

“Product” -> “Archive” will open the Organizer, and all you to upload the bundle to Apple. Once it’s uploaded, it will sit in a Processing Content state for a few minutes. Once it has changed to Waiting for Screenshot then it can be downloaded from the Sandbox. (For the download to be live on the production server, you would have to upload a screenshot and then wait for the download to be approved.)

Summary

In order to support downloadable content for your IAP, you need to:

Then of course, you need to modify your app to actually use the downloaded content!


Older: Mobile Site Breakdown - audileds.com

Newer: Saving objective-c method calls


View Comments

Related Posts

Recent Posts