Update note: This tutorial was updated for iOS 8 and Swift by Ray Fix. Original tutorial was by site editor-in-chief Ray Wenderlich.
One of the great things about being an iOS devleoper is that there are a variety of models you can use to make money off of your apps, including paid, free with ads, and in-app purchases.
In-app purchases are a particularly compelling option, for several reasons:
- You can earn more money than just the price of your app. Some users are willing to spend a lot more on extra content!
- You can release your app for free (which is a no-brainer download for most people), and if they enjoy it they can purchase more.
- Once you have it implemented, you can keep adding additional content to the same app (rather than having to make a new app to earn more money!)
You can use In-App Purchases with varying business models. For example, the Ray Wenderlich app Wild Fables comes with three stories included with more available as in-app purchases. Battle Map 2 is an example of a paid app with optional extra content as in-app purchases.
In this tutorial, you’ll learn how to use in-app purchases to unlock local content embedded in your app.
This tutorial assumes that you are familiar with basic Swift and iOS programming concepts. If these concepts are new to you, check out some of the other tutorials on this site.
Getting Started
For this tutorial, you’re going to make a little app called In App Rage. The app allows users to buy rage comics, sometimes called “F7U12”. Readers of this site will undoubtedly recognize the genre. They’re basically funny little comics where someone goes through a common and frustrating situation, resulting in a wild rage or other humorous expression.
Before you can start coding, you’ll need to create a placeholder app in the iOS Developer Center and iTunes Connect.
First, log into the iOS Developer Center. Select Identifiers under iOS Apps and then select the App IDs tab. Click the + button and complete the form like the following:
You must change the bundle identifier to have your own unique prefix. A common practice is to use your domain name in reverse. If all fails use a made-up one based on your name or something else unique.
Notice that In-App Purchase (and GameKit) are enabled by default. When you’re done, click Continue and then Submit. Viola – you have a new App ID! Now you’ll use it to create a new app in iTunes Connect.
Log onto iTunes Connect, click My Apps then + to add a new iOS App. If you’re prompted to choose an app type, select New iOS App (obviously). Then complete the form as shown below:
If you are quick in getting to this step, you might notice that the Bunde ID is not showing up in the dropdown list. Apparently, this takes time to propagate. Take this opportunity to obey your Apple Watch, stand up, and walk around the block. Refresh the page when you get back and hopefully it will be there.
Also you’ll have to tweak the app Name, because app names need to be unique across the App Store. I’ve added an entry for this one. Maybe replace the RAF with your own initials.
Managing In App Purchases
The reason you just created a placeholder app is that before you can code in-app purchases, you have to set them up in iTunes Connect. Now that you have a placeholder app, just click In-App Purchases, as shown below:
Then click Create New in the upper left corner:
You will get to a screen that lets you select the type of In-App Purchase you want to add. Note that there are are two types of frequently-used In-App Purchases:
- Consumables: can be bought more than once and can be used up. Like extra lives, in-game currency, temporary power-ups, and the like.
- Non-Consumables. Something that you buy once, and expect to have it permanently. Things like extra levels, unlockable content, etc.
For In App Rage, you are going to be selling comics. Once the user purchases them, they should always have them, so choose Non-Consumable.
We’ll talk more about how to allow the user to restore the non-consumable content they purchased on other devices later.
There is no such requirement for consumables though – consumables can be for just the device the user bought them on. If you want consumables to be cross-device, you’d have to implement that yourself with iCloud or some other technology.
Next, you will be taken to a page to enter some information about your in-app purchase. Fill in the fields according to the screenshot below:
Let’s cover what each of these fields means:
- Reference Name: This is what shows up in iTunes Connect for this in-app purchase. It can be whatever you want since you won’t see it anywhere in the app.
- Product ID: Also known as “product identifier” in the Apple docs, this is the unique string that identifies your in-app purchase. Usually it’s best to start out with your Bundle ID, and then append a unique name for the purchase. In order for the sample code in this tutorial to work well without modification, you will need to use a certain naming convention discussed below.
- Cleared for Sale: If this in-app purchase is OK for users to purchase as soon as the app becomes available.
- Price Tier: How much this in-app purchase should cost.
After you’ve set that up, scroll down to the Language section and click Add Language. Fill out the form that pops up with the following information:
This information will be returned to you when you query the App Store later on for the in-app purchases that are available. Prices will be in the correct currency for the part of the world you are selling in and you can also enable/disable these purchases on the fly. Don’t worry about the descriptions – you won’t be using them in this tutorial, so you can just use the Display Name for those.
You will also notice that there are fields for review Notes and Screenshots. While ultimately you will need to provide these for Apple’s review process, you do not need them for testing in the sandbox.
In order for the sample app to work without changes, your Product ID should take the form “YYYYY.XXXXX” where YYYYY is your unique name (mine was org.rayfix.inapprage) and XXXXX is the name of the image to be displayed. The names are: nightlyrage
, girlfriendofdrummer
, iphonerage
, and updog
.
You can now click Save. Great you just created your first In-App purchase. Now, repeat the process three more times for the remaining purchases. When you’re done, your purchases should look like this:
You might notice that this process takes a while. I could imagine it gets annoying if you have a ton of in-app purchases in your app. Luckily you’re not in that situation, If you are in your app, draw me a rage comic :]
Quick Tour of the Starter Project
Download the starter project, unzip it and open in Xcode. Open MasterViewController.swift. This class imports StoreKit
and displays a table view of available in-app purchases. Purchases are stored in an array of SKProduct
objects. Each row (if not purchased) has a “Buy” button that lets you purchase the product. An NSNumberFormatter
is used to show the localized price. Once purchased, you can view the comic for that purchase using the detail view. Finally, a “Restore” button lets you restore all previous purchases.
You will notice that MasterViewController.swift is using an object called RageProducts.store of type IAPHelper
to do the heavy lifting. However, this class is currently stubbed out. If you build and run the app you will not run at this point.
Matching the Identifiers
For anything to work, you need to match up the bundle identifier and product identifiers in your app to the same ones you entered in iTunes Connect.
Select your project target in Project Navigator and then the General tab Change the value of Bundle Identifier to match. I used “org.rayfix.inappragedemo” but yours will be different.
Next change the product identifiers to match what you entered. Open RageProducts.swift and notice the list of four in-app purchases.
You probably only need to change the Prefix
constant to match what you used in the previous sections. (It is marked with the TODO comment.)
Listing In-App Purchases
The RageProduct.store
is an instance of IAPHelper
. This object interacts with the StoreKit API to list and perform purchases. Open IAPHelper.swift and notice that is not yet implemented. You will now do so.
The first thing you need to do is get a list of in-app purchases from Apple’s server. Add the following private properties to IAPHelper
class.
/// MARK: - Private Properties // Used to keep track of the possible products and which ones have been purchased. private let productIdentifiers: Set<ProductIdentifier> private var purchasedProductIdentifiers = Set<ProductIdentifier>() // Used by SKProductsRequestDelegate private var productsRequest: SKProductsRequest? private var completionHandler: RequestProductsCompletionHandler? |
You will use these properties to perform your requests and keep track of what purchases have already been made. When you add this code you will immediately see a compiler error in init(productIdentifiers:)
. This is because the initialization rules of Swift dictate that you must initialize all class properties before calling super.init()
. Fix that by adding the following to init(productIdentifiers:)
:
self.productIdentifiers = productIdentifiers |
An IAPHelper
is created by passing in the set of product identifiers supported. This is how RageProducts
creates its store
instance. Next, replace the implementation of requestProductsWithCompletionHandler(_:)
/// Gets the list of SKProducts from the Apple server calls the handler with the list of products. public func requestProductsWithCompletionHandler(handler: RequestProductsCompletionHandler) { completionHandler = handler productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) productsRequest?.delegate = self productsRequest?.start() } |
This code saves away the user’s completion handler so that it can be executed later. It then creates a request and fires the request off to Apple. Since IAPHelper
does not yet conform to the SKProductsRequestDelegate
protocol, you’ll see another compiler error. Fix that by adding the following IAPHelper
extension at the end of the file:
// MARK: - SKProductsRequestDelegate extension IAPHelper: SKProductsRequestDelegate { public func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) { println("Loaded list of products...") let products = response.products as! [SKProduct] completionHandler?(success: true, products: products) clearRequest() // debug printing for p in products { println("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)") } } public func request(request: SKRequest!, didFailWithError error: NSError!) { println("Failed to load list of products.") println("Error: \(error)") clearRequest() } private func clearRequest() { productsRequest = nil completionHandler = 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, productsRequest(_:didFailWithError:)
is called. In either case, when the request finishes you clear the request and completion handler with clearRequest()
.
Build and run now. You should now see a list of products in the table view. This part should work even in the simulator without a sandbox account.
Didn’t work? If this didn’t work for you, there are a number of things to check (this list courtesy of itsme.manish and abgtan from the forums:
- Go to Settings\iTunes & App Stores, log out of any account, and try again so you’re sure you’re using a Sandbox account.
- Does your project’s Bundle ID match your App ID?
- Check this link – if it doesn’t respond, the iTunes sandbox may be down.
- Have you enabled In-App Purchases for your App ID?
- Are you using the full product ID when when making an SKProductRequest?
- Have you waited several hours since adding your product to iTunes Connect?
- Are your bank details active on iTunes Connect?
- Have you tried deleting the app from your device and reinstalling?
Tried all that and still stuck? Try the old forum thread or this thread’s comments for 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. If a product identifier is contained in this set, the user has purchased the item. The method for checking this is straightforward. Find the isProductPurchased(_:)
function and replace the implementation with the following:
return purchasedProductIdentifiers.contains(productIdentifier) |
Every time your app starts you don’t want to have to go to Apple’s server to find out if a particular purchase has been made. It is a good idea to save this information locally. You will use NSUserDefaults
to save purchasedProductIdentifiers.
Add the following before the call to super
in your init(productIdentifiers:)
method:
for productIdentifier in productIdentifiers { let purchased = NSUserDefaults.standardUserDefaults().boolForKey(productIdentifier) if purchased { purchasedProductIdentifiers.insert(productIdentifier) println("Previously purchased: \(productIdentifier)") } else { println("Not purchased: \(productIdentifier)") } } |
For each of your product identifiers, you check to see if the value is stored in NSUserDefaults
and if it is, you insert it into the set. Later, you’ll also add an identifier to the set after a purchase is made.
Making Purchases (Show Me The Money!)
That’s great, but you need to be able to make purchases. How do you do that? That is what you will implement next. Still in IAPHelper.swift, replace the purchaseProduct(_:)
implementation with the following:
/// Initiates purchase of a product. public func purchaseProduct(product: SKProduct) { println("Buying \(product.productIdentifier)...") let payment = SKPayment(product: product) SKPaymentQueue.defaultQueue().addPayment(payment) } |
This creates a payment object using a SKProduct
(which you got from the server) to add to a payment queue. There’s a singleton SKPaymentQueue
object called defaultQueue()
. Boom! Money in the bank!
How do you know if the payment went through? For that, you need your IAPHelper to observer transactions happening on the SKPaymentQueue
. Go back to your init(productIdentifiers:)
method and add the following line to the end of the function, right after super.init()
.
SKPaymentQueue.defaultQueue().addTransactionObserver(self) |
This results in a compiler error because IAPHelper
needs to conform to the SKPaymentTransactionObserver
protocol. If you think of the compiler as a helpful todo list generator, this is the next item on the list!
Go to the end of the file and add the following extension and methods:
extension IAPHelper: SKPaymentTransactionObserver { /// This is a function called by the payment queue, not to be called directly. /// For each transaction act accordingly, save in the purchased cache, issue notifications, /// mark the transaction as complete. public func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) { for transaction in transactions as! [SKPaymentTransaction] { 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) { println("completeTransaction...") provideContentForProductIdentifier(transaction.payment.productIdentifier) SKPaymentQueue.defaultQueue().finishTransaction(transaction) } private func restoreTransaction(transaction: SKPaymentTransaction) { let productIdentifier = transaction.originalTransaction.payment.productIdentifier println("restoreTransaction... \(productIdentifier)") provideContentForProductIdentifier(productIdentifier) SKPaymentQueue.defaultQueue().finishTransaction(transaction) } // Helper: Saves the fact that the product has been purchased and posts a notification. private func provideContentForProductIdentifier(productIdentifier: String) { purchasedProductIdentifiers.insert(productIdentifier) NSUserDefaults.standardUserDefaults().setBool(true, forKey: productIdentifier) NSUserDefaults.standardUserDefaults().synchronize() NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperProductPurchasedNotification, object: productIdentifier) } private func failedTransaction(transaction: SKPaymentTransaction) { println("failedTransaction...") if transaction.error.code != SKErrorPaymentCancelled { println("Transaction error: \(transaction.error.localizedDescription)") } SKPaymentQueue.defaultQueue().finishTransaction(transaction) } } |
That is a lot of code! So let’s go through it in detail. paymentQueue(_:updatedTransactions:)
is the only method actually required by the protocol. It gets called when one or more transactions’ states change. This method goes through an array of updated transactions and looks at their state. Based on that state it calls other methods defined here: 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.
Restoring Payments
If the user deletes and re-installs the app, or if they install it on another device, they need to be able to recover their purchases. In fact, Apple may reject your app if you do not implement the ability to restore non-consumable purchases.
You are already listening for when purchases have been restored. But you need to write the method that initiates it. Find the restoreCompletedTransactions()
method and add the following to it:
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.
In App Purchases, Accounts, and the Sandbox
While you’re running your app in Xcode, you’re not making transactions against the real in-app purchase servers – you’re running against sandbox servers.
This means you can buy things without fear of getting charged, etc. But you need to set up a test account, and also make sure you’re logged out of the store with your real account if you’re testing on device.
To make accounts, log onto iTunes Connect and click Users and Roles. Click Sandbox User then follow the buttons to create a test user.
Then go to your iPhone and make sure you’re logged out of your current account. To do this, go to the Settings app and tap iTunes & App Store. Tap your iCloud account name, and then select Sign Out.
Finally, go ahead and build and run your app and attempt to purchase a rage comic. Enter your test user account information and if all goes well, it should purchase with a happy check mark next to it. You can tap on it and see the comic! The list of purchases should look like this:
Payment Permissions
Some devices and accounts may not permit in-app purchase. This can happen, for example, if parental controls are set to disallow it. Apple requires that you handle this situation gracefully; not doing so will likely result in an app rejection.
Open IAPHelper.swift and add the following method to the class:
public class func canMakePayments() -> Bool { return SKPaymentQueue.canMakePayments() } |
When canMakePayments()
is false, your master view controller should display cells differently. For example, don’t show a “Buy” button, and simply say “Not Available” instead of listing the price.
To do this, open MasterViewController.swift and update the implementation of tableView(_:cellForRowAtIndexPath)
as follows:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell let product = products[indexPath.row] cell.textLabel?.text = product.localizedTitle if RageProducts.store.isProductPurchased(product.productIdentifier) { cell.accessoryType = .Checkmark cell.accessoryView = nil cell.detailTextLabel?.text = "" } else if IAPHelper.canMakePayments() { priceFormatter.locale = product.priceLocale cell.detailTextLabel?.text = priceFormatter.stringFromNumber(product.price) var button = UIButton(frame: CGRect(x: 0, y: 0, width: 72, height: 37)) button.setTitleColor(view.tintColor, forState: .Normal) button.setTitle("Buy", forState: .Normal) button.tag = indexPath.row button.addTarget(self, action: "buyButtonTapped:", forControlEvents: .TouchUpInside) cell.accessoryType = .None cell.accessoryView = button } else { cell.accessoryType = .None cell.accessoryView = nil cell.detailTextLabel?.text = "Not Available" } return cell } |
This implementation will make the display more appropriate in the case where payments cannot be made with the device.
And there you have it – an app with in-app purchase!
Where To Go From Here?
Here is In App Rage Final.zip with all of the code you’ve developed above. Feel free to re-use the in-app purchase helper class.
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.
In-app purchases 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 any of you have questions or comments about this tutorial 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 Purchases Tutorial: Getting Started appeared first on Ray Wenderlich.