Update note: This in-app purchase tutorial was updated for iOS 9.3 and Swift 2.2 by Ed Sasena. The original tutorial was written by site editor-in-chief Ray Wenderlich.
One of the great things about being an iOS developer is the variety of methods available for making money from apps, namely: paid, free with ads, and in-app purchases.
An in-app purchase (or ‘IAP‘), the focus of this tutorial, allows developers to charge users for specific functionality or content while using an app. Implementing IAPs is particularly compelling for several reasons:
- It’s an extra way to earn money, in addition to simply selling the app for a fee upfront. Some users are willing to spend a lot more on extra content.
- An app can be offered for free, which makes it a no-brainer download for most people; free apps will typically get many more downloads than paid apps. If users enjoy the app, then they can purchase more content or functionality later.
- You could also display advertisements to the user in a free app, with an option to remove them by purchasing an IAP.
- Following the initial release of an app, new paid content can be added to the same app instead of having to develop a brand new app to earn more money.
In this in-app purchase tutorial you’ll leverage IAPs to unlock extra content embedded in an app. You’ll need to be familiar with basic Swift and iOS programming concepts. If these are unfamiliar topics, then check out our range of Swift tutorials before getting started. You’ll also need a paid developer account, with access to both the iOS Developer Center and iTunes Connect.
Getting Started
In this in-app purchase tutorial, you’ll be building a small app called “Rage”, which allows users to buy rage comics. Readers of this site will undoubtedly recognize the genre. Rage comics typically involve someone facing a common, frustrating situation and reacting wildly in a humorous manner.
Download the starter project and open it in Xcode. Build and run to see what it does so far. The answer is: not a lot! You’ll see an empty table view, with a single “Restore” button in the navigation bar (which will be hooked up later to restore purchases).
Upon finishing this tutorial, there will be a list of rage comics listed in the table which you’ll be able to buy and then view. If you delete and reinstall the app, the “Restore” button will reinstate any previously purchased comics.
Head over to Xcode to take a quick look at the code. The main view controller is in MasterViewController.swift. This class displays the table view which will contain a list of available IAPs. Purchases are stored as an array of SKProduct
objects.
Notice that MasterViewController
is using an object called RageProducts.store
of type IAPHelper
to do the heavy lifting. Take a look at their respective code files, RageProducts.swift and IAPHelper.swift.
RageProducts
is a simple struct that contains some information about the products in the app, and IAPHelper
does all the important work of talking to StoreKit. The methods are all stubbed out at the moment, but you’ll fill them out in this tutorial to add IAP functionality to the app.
Before writing any code to incorporate IAP into an app, you’ll first need to do some setup in the iOS Developer Center and iTunes Connect. So let’s get busy.
Creating an App ID
First you’ll need to create an App ID. This will link together your app in Xcode, to iTunes Connect, to your in-app purchaseable products. Login to the iOS Developer Center, then select Certificates, Identifiers & Profiles.
Next, under iOS Apps, select Identifiers. Then select App IDs in the left pane. Click + in the upper right corner to create a new App ID.
Fill out the information for the new App ID. Enter “Rage IAP Tutorial App” for the Name, and leave the App ID Prefix set to your Team ID. Choose Explicit App ID, and enter a unique Bundle ID. A common practice is to use your domain name in reverse (for example, com.razeware.rageapp). Make note of the Bundle ID as it will be needed in the steps that follow.
Scroll down to the App Services section. Notice that In-App Purchase and GameKit are enabled by default. Click Continue and then Submit.
Congratulations! You have a new App ID! Next, you’ll create a matching app in iTunes Connect.
Creating an App in iTunes Connect
Before you can add IAPs to an app in iTunes Connect, make sure you have a “paid applications contract” in effect. Log in to iTunes Connect for the same developer account you just used to set up the App ID. Click on Agreements, Tax, and Banking.
If you see a section entitled Request Contracts containing a row for Paid Applications, then click the Request button. Fill out all the necessary information and submit it. It may take some time for your request to be approved. Sit tight!
Otherwise, if you see Paid Applications listed under Contracts In Effect, then it looks like you’ve already done this step! Nice job!
Now to create the app record itself, click iTunes Connect in the upper left corner of the page, and then click My Apps.
Next, click + in the upper left corner of the page and select New App to add a new app record. Fill out the information as shown here:
You won’t be able to use the exact same app Name that you see here, because app names need to be unique across the App Store. Perhaps add your own initials after the example title shown in the screenshot above.
Click Create and you’re done!
Creating In-App Purchase Products
When offering IAP within an an app, you must first add an entry for each individual purchase within iTunes Connect. If you’ve ever listed an app for sale in the store, it’s a similar process and includes things like choosing a pricing tier for the purchase. When the user makes a purchase, the App Store handles the complex process of charging the user’s iTunes account.
There are a whole bunch of different types of IAP you can add:
- Consumable: These can be bought more than once and can be used up. These are things such as extra lives, in-game currency, temporary power-ups, and the like.
- Non-Consumable: Something that you buy once, and expect to have permanently such as extra levels and unlockable content.
- Non-Renewing Subscription: Content that’s available for a fixed period of time.
- Auto-Renewing Subscription: A repeating subscription such as a monthly Netflix subscription.
You can only offer In-App Purchases for digital items, and not for physical goods or services. For more information about all of this, check out Apple’s full documentation on Creating In-App Purchase Products.
Now, while viewing your app’s entry in iTunes Connect, click on the Features tab and then select In-App Purchases. To add a new IAP product, click the + to the right of In-App Purchases. When a user purchases a rage comic in your app, you’ll want them to always have access to it, so select Non-Consumable.
Next, fill out the details for the IAP as follows:
- Reference Name: A nickname identifying the IAP within iTunes Connect. This name does not appear anywhere in the app. The title of the comic you’ll be unlocking with this purchase is “Girlfriend of Drummer“, so enter that here.
- Product ID: This is a unique string identifying the IAP. Usually it’s best to start with the Bundle ID and then append a unique name specific to this purchasable item. For this tutorial, make sure you append “
GirlfriendOfDrummerRage
“, as this will be used later within the app to look up the comic to unlock. So, for example:com.theNameYouPickedEarlier.Rage.GirlFriendOfDrummerRage
. - Cleared for Sale: Enables or disables the sale of the IAP. You want to enable it!
- Price Tier: The cost of the IAP. Choose Tier 1.
Now scroll down to the In-App Purchase Details section and click Add Language. The language section allows you to provide localizations for the names of your products. Select your language of choice, and enter “Girlfriend of Drummer” again for both the Display Name and the Description. Click Save. Great! You’ve created your first IAP product.
There’s one more step required before you can delve into some code. When testing in-app purchases in a development build of an app, Apple provides a test environment which allows you to ‘purchase’ your IAP products without creating financial transactions.
These special test purchases can only be made by a special “Sandbox Tester” user account in iTunes Connect. So let’s create one. We’re almost at the code, I promise!
Creating a Sandbox User
In iTunes Connect, click iTunes Connect in the top left corner of the window to get back to the main menu. Select Users and Roles, then click the Sandbox Testers tab. Click + next to the “Tester” title.
Fill out the information and click Save when you’re done. You can make up a first and last name for your test user, but the email address chosen must be a real email address as a verification will be sent to the address by Apple. Once you receive that email, be sure to click the link in it to verify your address.
The email address you enter should also NOT already be associated with an Apple ID account.
Note: Unfortunately, testing a new purchase of a non-consumable IAP requires a new sandbox tester (and email address) each time. Repeated purchases using the same sandbox tester will be treated as restoring an already purchased item, so any code specific to new purchases will not be exercised.If multiple test runs through new purchase code are necessary and your email provider does not support qualifiers, then consider setting up a consumable IAP just for testing purposes. Delete the app on your device after each test and the purchase of a consumable IAP will be considered a new purchase.
Great – you now have a test user. You can finally implement IAP in your app!
Project Configuration
For everything to work correctly, it’s really important that the bundle identifier and product identifiers in the app match the ones you just created in the Developer Center and in iTunes Connect.
Head over to the starter project in Xcode. Select the Rage project in the project navigator, then select it again under Targets. Select the General tab, and enter the bundle ID you used earlier.
Next select the Capabilities tab. Scroll down to In-App Purchase and toggle the switch to ON.
There’s one other place that you need to set the bundle ID. Open RageProducts.swift. Find the following line of code:
private static let Prefix = "com.razeware.Rage." |
And change it to use the bundle ID you created earlier. Don’t forget to include the final period after "Rage."
, since this is a prefix that will precede each IAP product name.
Notice that the lines below include a reference to the IAP product you created: GirlfriendOfDrummerRage
. This is why it’s important that you set the product ID correctly in iTunes Connect:
public static let GirlfriendOfDrummerRage = Prefix + "GirlfriendOfDrummerRage" private static let productIdentifiers: Set = [RageProducts.GirlfriendOfDrummerRage] |
Listing In-App Purchases
The store
property of RageProduct
is an instance of IAPHelper
. As was briefly mentioned earlier, this object interacts with the StoreKit API to list and perform purchases. Your first task will be to update IAPHelper
to retrieve a list of IAPs (although there is only one so far) from Apple’s servers.
Open IAPHelper.swift. At the top of the class, add the following private property:
private let productIdentifiers: Set |
Next, add the following to init(productIds:)
before the call to super.init()
:
self.productIdentifiers = productIds |
An IAPHelper
instance is created by passing in a set of product identifiers. This is how RageProducts
creates its store
instance.
Next, add these other private properties just under the one you added a moment ago:
private var purchasedProductIdentifiers = Set() private var productsRequest: SKProductsRequest? private var productsRequestCompletionHandler: ProductsRequestCompletionHandler? |
purchasedProductIdentifiers
will track which items have been purchased. The other two properties will be used by the SKProductsRequest
delegate to perform requests from Apple.
Next, still in IAPHelper.swift replace the implementation of requestProducts(_:)
with the following:
public func requestProducts(completionHandler: ProductsRequestCompletionHandler) { productsRequest?.cancel() productsRequestCompletionHandler = completionHandler productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) productsRequest!.delegate = self productsRequest!.start() } |
This code saves the user’s completion handler for future execution. It then creates and initiates a request to Apple via an SKProductsRequest
object. There’s one problem: the code declares IAPHelper
as the request’s delegate, but it doesn’t yet conform to the SKProductsRequestDelegate
protocol.
To fix this, add the following extension to the very end of IAPHelper.swift, after the last curly brace:
// MARK: - SKProductsRequestDelegate extension IAPHelper: SKProductsRequestDelegate { public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) { print("Loaded list of products...") let products = response.products productsRequestCompletionHandler?(success: true, products: products) clearRequestAndHandler() for p in products { print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)") } } public func request(request: SKRequest, didFailWithError error: NSError) { print("Failed to load list of products.") print("Error: \(error.localizedDescription)") productsRequestCompletionHandler?(success: false, products: nil) clearRequestAndHandler() } private func clearRequestAndHandler() { productsRequest = nil productsRequestCompletionHandler = nil } } |
This extension is used to get a list of products, their titles, descriptions and prices from Apple’s server by implementing the two methods required by the SKProductsRequestDelegate
protocol.
productsRequest(_:didReceiveResponse:)
is called when the list is succesfully retrieved. It receives an array of SKProduct
objects and passes them to the previously saved completion handler. The handler reloads the table with new data. If a problem occurs, request(_:didFailWithError:)
is called. In either case, when the request finishes, both the request and completion handler are cleared with clearRequestAndHandler()
.
Build and run. Hooray! A list of products (only one so far) is displayed in the table view! It took some work, but you got there in the end.
If the run was unsuccessful and you didn’t see any products, then there are a number of things to check. (This list is courtesy of itsme.manish and abgtan from the forums.)
- Does the project’s Bundle ID match the App ID from the iOS Development Center?
- Check Apple Developer System Status. Alternatively, try this link. If it doesn’t respond with a status value, then the iTunes sandbox may be down. The status codes are explained in Apple’s Validating Receipts With the App Store documentation.
- Have IAPs been enabled for the App ID? (Did you select “Cleared for Sale” earlier?)
- Is the full product ID being used when making an
SKProductRequest
? (Check theproductIdentifiers
property ofRageProducts
.) - Have you waited several hours since adding your product to iTunes Connect? Product additions may be active immediately or may take some time.
- Is the Paid Applications Contract in effect on iTunes Connect?
- Have you tried deleting the app from your device and reinstalling?
Still stuck? It’s time to make another rage comic and/or try this tutorial’s comments for a discussion with other readers.
Purchased Items
You want to be able to determine which items are already purchased. To do this you will use the purchasedProductIdentifiers
property you added earlier. If a product identifier is contained in this set, the user has purchased the item. The method for checking this is straightforward.
In IAPHelper.swift, replace the return
statement in isProductPurchased(_:)
with the following:
return purchasedProductIdentifiers.contains(productIdentifier) |
Saving purchase status locally alleviates the need to request the information from Apple’s server every time the app starts. purchasedProductIdentifiers
are saved using NSUserDefaults
.
Still in IAPHelper.swift, add the following before the call to super.init()
in init(productIds:)
:
for productIdentifier in productIds { let purchased = NSUserDefaults.standardUserDefaults().boolForKey(productIdentifier) if purchased { purchasedProductIdentifiers.insert(productIdentifier) print("Previously purchased: \(productIdentifier)") } else { print("Not purchased: \(productIdentifier)") } } |
For each product identifier, you check whether the value is stored in NSUserDefaults
. If it is, then the identifier is inserted into the purchasedProductIdentifiers
set. Later, you’ll add an identifier to the set following a purchase.
NSUserDefaults
plist, and modify it to ‘unlock’ purchases. If this sort of thing concerns you, then it’s worth checking out Apple’s documentation on Validating App Store Receipts – this allows you to verify that a user has made a particular purchase.Making Purchases (Show Me The Money!)
Knowing what a user has purchased is great, but you still need to be able to make the purchases in the first place! Implementing purchase capability seems like a logical next step.
Still in IAPHelper.swift, replace buyProduct(_:)
with the following:
public func buyProduct(product: SKProduct) { print("Buying \(product.productIdentifier)...") let payment = SKPayment(product: product) SKPaymentQueue.defaultQueue().addPayment(payment) } |
This creates a payment object using an SKProduct
(retrieved from the Apple server) to add to a payment queue. The code utilizes a singleton SKPaymentQueue
object called defaultQueue()
. Boom! Money in the bank. Or is it? How do you know if the payment went through?
Payment verification is achieved by having IAPHelper
observe transactions happening on the SKPaymentQueue
. Before setting up IAPHelper
as an SKPaymentQueue
transactions observer, the class must conform to the SKPaymentTransactionObserver
protocol.
Go to the very bottom (after the last curly brace) of IAPHelper.swift and add the following extension:
// MARK: - SKPaymentTransactionObserver extension IAPHelper: SKPaymentTransactionObserver { public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch (transaction.transactionState) { case .Purchased: completeTransaction(transaction) break case .Failed: failedTransaction(transaction) break case .Restored: restoreTransaction(transaction) break case .Deferred: break case .Purchasing: break } } } private func completeTransaction(transaction: SKPaymentTransaction) { print("completeTransaction...") deliverPurchaseNotificatioForIdentifier(transaction.payment.productIdentifier) SKPaymentQueue.defaultQueue().finishTransaction(transaction) } private func restoreTransaction(transaction: SKPaymentTransaction) { guard let productIdentifier = transaction.originalTransaction?.payment.productIdentifier else { return } print("restoreTransaction... \(productIdentifier)") deliverPurchaseNotificatioForIdentifier(productIdentifier) SKPaymentQueue.defaultQueue().finishTransaction(transaction) } private func failedTransaction(transaction: SKPaymentTransaction) { print("failedTransaction...") if transaction.error!.code != SKErrorCode.PaymentCancelled.rawValue { print("Transaction Error: \(transaction.error?.localizedDescription)") } SKPaymentQueue.defaultQueue().finishTransaction(transaction) } private func deliverPurchaseNotificatioForIdentifier(identifier: String?) { guard let identifier = identifier else { return } purchasedProductIdentifiers.insert(identifier) NSUserDefaults.standardUserDefaults().setBool(true, forKey: identifier) NSUserDefaults.standardUserDefaults().synchronize() NSNotificationCenter.defaultCenter().postNotificationName(IAPHelper.IAPHelperPurchaseNotification, object: identifier) } } |
That’s a lot of code! A detailed review is in order. Fortunately, each method is quite short.
paymentQueue(_:updatedTransactions:)
is the only method actually required by the protocol. It gets called when one or more transaction states change. This method evaluates the state of each transaction in an array of updated transactions and calls the relevant helper method: completeTransaction(_:)
, restoreTransaction(_:)
or failedTransaction(_:)
.
If the transaction was completed or restored, it adds to the set of purchases and saves the identifier in NSUserDefaults
. It also posts a notification with that transaction so that any interested object in the app can listen for it to do things like update the user interface. Finally, in both the case of success or failure, it marks the transaction as finished.
If the transaction was completed or restored, deliverPurchaseNotificatioForIdentifier
gets called to add to the set of purchased items and save the identifier in NSUserDefaults
. It also posts a notification with that product’s identifier so that any interested object in the app can listen and do things like update the user interface. Finally, in both the case of success or failure, it marks the transaction as finished by calling finishTransaction
on the payment queue.
All that’s left is to hook up IAPHelper
as a payment transaction observer. Still in IAPHelper.swift, go back to init(productIds:)
and add the following line right after super.init()
.
SKPaymentQueue.defaultQueue().addTransactionObserver(self) |
Making a Sandbox Purchase
Build and run the app – but to test out purchases, you’ll have to run it on a device. The sandbox tester created earlier can be used to perform the purchase without getting charged. If only I could have a sandbox tester to do my grocery shopping! Here’s how to use the tester account:
Go to your iPhone and make sure you’re logged out of your normal App Store account. To do this, go to the Settings app and tap iTunes & App Store.
Tap your iCloud account name and then tap Sign Out.
Build and run! You’ll see your product listed in the app. To begin the purchase process, tap the Buy button.
An alert will appear prompting you to log in. Tap Use Existing Apple ID, and enter the login details for the sandbox tester account that you created earlier.
Confirm the purchase by tapping Buy. The alert view notes that the purchase is being made in the sandbox as a reminder that you won’t be charged for it.
Finally, an alert view will appear confirming the purchase was successful. Once the purchase process has been completed, a checkmark appears next to the purchased item. Tap on the purchased item to enjoy your new comic.
Finally you get to see this “Girlfriend of Drummer” rage comic that you’ve been hearing so much about!
Restoring Purchases
If the user deletes and re-installs the app or installs it on another device, then they need the ability to access previously purchased items. In fact, Apple may reject an app if it cannot restore non-consumable purchases.
As a purchase transaction observer, IAPHelper
is already being notified when purchases have been restored. The next step is to react to this notification by restoring the purchases.
Open IAPHelper.swift. In the StoreKit API extension, replace restorePurchases()
with the following:
public func restorePurchases() { SKPaymentQueue.defaultQueue().restoreCompletedTransactions() } |
That was almost too easy! You’ve already set the transaction observer and implemented the method to handle restoring transactions in the previous step.
To test this out, after you’ve made a purchase (which you should’ve done in the previous step), delete the app from your device. Build and run again, and then tap the Restore button. You should see a checkmark appear next to the previously purchased product.
Payment Permissions
Some devices and accounts may not permit an in-app purchase. This can happen, for example, if parental controls are set to disallow it. Apple requires this situation to be handled gracefully. Not doing so will likely result in an app rejection.
Open IAPHelper.swift again. In the StoreKit API extension, replace the return
statement in canMakePayments()
with this line:
return SKPaymentQueue.canMakePayments() |
Product cells should behave differently depending on the value returned by canMakePayments()
. For example, if canMakePayments()
returns false
, then the Buy button should not be shown and the price should be replaced by “Not Available”.
To accomplish this, open ProductCell.swift and replace the entire implementation of the product
property’s didSet
handler with the following:
didSet { guard let product = product else { return } textLabel?.text = product.localizedTitle if RageProducts.store.isProductPurchased(product.productIdentifier) { accessoryType = .Checkmark accessoryView = nil detailTextLabel?.text = "" } else if IAPHelper.canMakePayments() { ProductCell.priceFormatter.locale = product.priceLocale detailTextLabel?.text = ProductCell.priceFormatter.stringFromNumber(product.price) accessoryType = .None accessoryView = self.newBuyButton() } else { detailTextLabel?.text = "Not available" } } |
This implementation will display more appropriate information when payments cannot be made with the device. And there you have it – an app with in-app purchase!
Where To Go From Here?
You can download the final project with all of the code developed above. Feel free to re-use the IAP helper class in your own projects!
The In-App Purchase Video Tutorial Series by Sam Davies covers all of the topics introduced here, but goes to the next level in Part 3 where he talks about validating receipts.
One shortcoming of the sample app is that it doesn’t indicate to the user when it is communicating with Apple. A possible improvement would be to display a spinner or HUD control at appropriate times. This UI enhancement, however, is beyond the scope of this tutorial. For more information on HUD controls, check out The iOS Apprentice 3 tutorial.
Apple has a great landing page for in-app purchase: In-App Purchase for Developers. It collects together links to all the relevant documentation and WWDC videos.
IAPs can be an important part of your business model. Use them wisely and be sure to follow the guidelines about restoring purchases and failing gracefully, and you’ll be well on your way to success!
If you have any questions or comments about this in-app purchase tutorial, then please join the forum discussion below!
And to end things off with a laugh, here’s a great iOS app rage comic made by Jayant C Varma from the original Objective-C version of this tutorial! :]
The post In-App Purchase Tutorial: Getting Started appeared first on Ray Wenderlich.