In-App Purchase – How to download content hosted on the Apple server

Debugging tips

One of the most common errors you could get while trying to update the package is this one:

deliveryError

This means that the product ID you have setup in iTunes Connect and the one in the Xcode project are different. If in iTunes Connect you’ve only used CatPhotos in the product ID field and not com.masteringios.Photos.CatPhotos, then you should check the ContentInfo.plist in the CatPhotos folder and verify that the IAPProductIdentifier has the correct value. You can’t change the product ID in iTunes Connect after you’ve created the product so change the value in ContentInfo.plist.

If you have changed the IAPProductIdentifier value you have to recreate the archive and then export it again as a package.

Hopefully you got the big green checkmark and you’re ready to go to the next steps:

deliverySuccess

Just to check that everything is ok, go to iTunes Connect and check the status of the product. It should now be “Ready to Submit”. This concludes the configuration part of this demo. We managed to create the package and upload it to iTunes Connect. Next we’re going to see how to write code to download the photos and display them in the app.

Downloading content from the Apple server

In order to download the content hosted to the Apple server we’re going to use our transaction queue observer. When the user purchases a product that has associated content hosted on Apple’s server, the transaction passed to the transaction queue observer includes an instance of SKDownload. This allows you to download the associated content. The downloads property of the transaction queue indicated whether there is associated content or not (if nil then there is no associated content). If the downloads property is not nil then use it as a parameter to startDownloads: method on the payment queue.

if let downloads = transaction.downloads {
    SKPaymentQueue.defaultQueue().startDownloads(downloads)
}

Add this code in the completeTransaction: method of the PaymentTransactionObserver class. Unlike downloading apps, downloading content doesn’t automatically require a Wi-Fi connection for content larger than a certain size. If you’re going to use cellular networks to download large files it’s best to let the user know and give him the option to cancel.

Just as in the case of the payment transaction we need to implement the paymentQueue:updatedDownloads: and respond appropriately to different download states.

func paymentQueue(queue: SKPaymentQueue!, updatedDownloads downloads: [AnyObject]!) {
        
   for d in downloads {
            
       if let download = d as? SKDownload {
           switch (download.downloadState) {
           case .Waiting:
               waitingDownload(download)
           case .Active:
               activeDownload(download)
           case .Finished:
               finishedDownload(download)
           case .Failed:
               failedDownload(download)
           case .Cancelled:
               cancelledDownload(download)
           case .Paused:
               pausedDownload(download)
           }
       }
    }        
}

The methods called on each case clause aren’t implemented yet, we’re going to talk about it and add code to handle each state.

It’s time to talk about UI. It’s a good practice to indicate to the user what’s going on with the download. We’re going to add a progress view on the details page and a status label. These controls are going to be visible only when a download has been started. Go to the Main.storyboard and add them to the DetailViewController. Use auto layout to make sure they appear where you want them to. Make sure they are hidden at first. Add outlets for them so we can configure them programatically.

@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var statusLabel: UILabel!

The first download state that we’re going to process is Waiting. That indicates that the download has not started yet. Even if the download has not started yet we want to show the progress view and set the text of the status label to “waiting”. We’re going to continue using the NSNotificationCenter to post notifications and our UI will implement methods to react appropriately. First let’s go to the PaymentTransactionObserver class and implement the waitingDownload: method:

func waitingDownload(download: SKDownload) {
    NSNotificationCenter.defaultCenter().postNotificationName(IAPDownloadWaiting, object: download)
}

All we’re doing is posting the notification and passing the download object as a parameter. In the DetailViewController we want to sign up to receive this notification:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedDownloadWaitingNotification:", name: IAPDownloadWaiting, object: nil)

We add this code in the signUpForNotifications method. In receivedDownloadWaitingNotification: we want to make sure that the progress view and status label are showing and that the text on the status label is set to “waiting”:

func receivedDownloadWaitingNotification(notification: NSNotification) {        
    statusLabel.text = "waiting"
    progressView.progress = 0.0
        
    updateUIForDownloadInProgress(true)
}

First we set the proper values for the label and progress view and then we make them visible. Next let’s handle when the download is active. We will use the progress value from the SKDownload object that is sent with the notification to update the progress view. There is also a property called timeRemaining which sounds useful, but we’re not going to use in our example. First let’s see the implementation for activeDownload: in the PaymentTransactionObserver:

func activeDownload(download: SKDownload) {
    NSNotificationCenter.defaultCenter().postNotificationName(IAPDownloadActive, object: download)
}

Once again, there’s nothing more to do than post the notification. In the DetailViewController first sign up for the notification:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedDownloadActiveNotification:", name: IAPDownloadActive, object: nil)

and let’s implement the method. I’m not sure if we always get the download state waiting so I’m calling the method that will make sure the statusLabel and progressView are visible.

Let’s see what happens when the download is finished without any errors. Go to the PaymentTransactionObserver and implement the finishedDownload: method like this:

func finishedDownload(download: SKDownload) {
        
    moveDownloadedFiles(download)
        
    NSNotificationCenter.defaultCenter().postNotificationName(IAPDownloadFinished, object: nil)
        
    SKPaymentQueue.defaultQueue().finishTransaction(download.transaction)
}

The downloaded files are saved by Store Kit in the Caches directory. When the download completes the app is responsible for moving those files to the appropriate location. What is the appropriate location? According to Apple:

For content that can be deleted if the device runs our of disk space (and later re-downloaded by your app), leave the files in the Caches directory. Otherwise, move the files to the Documents folder and set the flag to exclude them from user backups.

In our case we could leave the files in the Caches directory. The app will work just fine without them and we will implement the restore functionality that allows the user to re-download the files. However, you could say that downloading large files isn’t such a good experience and you could decide to move the files to the Documents folder. You should take a moment and think about which option is right for you. I’m not going to talk here about the implementation of the moveDownloadedFiles: method, but you can find the code for it in the Github project.

After we post the notification to update the UI there is one more thing we must do. We need to finish the transaction. This will tell Store Kit that we have completed everything needed for the purchase. If we do not finish the transaction it will remain in the queue and the observer will be called every time the app is launched. Your app needs to finish every transaction, regardless of whether it succeeded or failed. We have already finished the transaction for the failed case, this is us finishing it for the success case.

How should we update the UI now that the download has finished? We could change the text on the buyButton to say “Purchased” and disable it. We can leave the progress view visible and change the statusLabel’s text to “download complete”. Let’s implement this:

func receivedDownloadFinishedNotification(notification: NSNotification) {
        
    statusLabel.text = "download complete"
    progressView.progress = 1.0
        
    buyButton.setTitle("Purchased", forState: .Normal)
    buyButton.enabled = false
    updateUIForPurchaseInProgress(false)
}

If the status of the download is Failed we want to alert the user and fail gracefully. The error property is going to provide more details regarding the reason why the download failed. There are a number of ways to improve the user experience even if the download failed. You could ask the user if you should remove the files or try again to download the files. We’re only going to display an error message. We could finish the transaction and the user can use the restore button to download the files. If we give the user options then the transaction should not be finished until we are sure that we don’t use it anymore (e.g. if you plan to resume the download later then don’t finish the transaction).

func failedDownload(download: SKDownload) {
        
    NSNotificationCenter.defaultCenter().postNotificationName(IAPDownloadFailed, object: download)
        
    SKPaymentQueue.defaultQueue().finishTransaction(download.transaction) 
}

And in DetailViewController implement the receivedDownloadFailedNotification: method like this:

func receivedDownloadFailedNotification(notification: NSNotification) {        
    let download = notification.object as? SKDownload
    let error = download?.error
        
    displayErrorAlert(error)
}

func displayErrorAlert(error: NSError) {        
    let alert = UIAlertController(title: "Error", message: error.localizedDescription , preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
    self.presentViewController(alert, animated: true, completion: nil)
}

The last two download states are Paused and Cancelled. In order to receive any of them we have to allow the user to pause or cancel the download. To implement this we would need to add buttons for Pause and Cancel in our UI. In order to pause or cancel the download we would need the SKDownload object, so we need to add a property for it. In the action method for each button we need to call pauseDownloads: or cancelDownloads: and pass as parameter an array that contains the download property. We also need to make some UI updates. There are many things you can do to improve the user experience when something goes wrong and deleting files or resuming download later when you have disk space or network connectivity is not in the scope of this tutorial. Have a look at the Github project, it should give you a good starting point to add all the extra implementation needed by your app requirements.

We have seen in this tutorial how to host content on Apple’s server and how to download it when the payment transaction is successful.  There is one more step that we haven’t talked about and that is restoring a purchase. Apple requires you to have a way to restore purchases and that’s what we’re going to talk about in the next tutorial.

I hope you enjoyed this post and that you now have an app that downloads content from the Apple server successfully. If you have any questions or feedback please let me know.

One thought on “In-App Purchase – How to download content hosted on the Apple server

  1. Thanks for the incredible tutorial, it has helped me to implement IAP downloads on my app. Could you clarify how to access the downloaded content? I already have the path to it (download.contentURL), but I’m not sure how to access the files inside it.

Leave a Reply

Your email address will not be published. Required fields are marked *

*