Chapter 34: In-App Purchases

Monetizing your apps is a common way to create wealth as a developer. Apple has made it easy to do this by integrating In-App Purchases.


What you will learn

  • How to create a Collection View
  • Navigating iTunes Connect for IAP's
  • Registering a App Id for IAP's
  • Creating multiple IAP tiers
  • Sandbox testing IAP's

Key Terms

  • IAP (in-app purchase)
  • UICollectionView
  • UICollectionCell
  • SKPayment
  • Non-Consumable
  • Restore Purchases
  • Sandbox

Resources

Download here: https://github.com/devslopes/book-assets/wiki

Setting up in-app purchases is easier than you’d think. I’ll take you through it step by step.
First, we need to make products in iTunes Connect * This requires a paid developer account.
Once we have them set up we need to request those products to see if they are available. Once we know they are available we can display the product info. Then the purchase can be made by creating an SKPayment and add that to the queue.

We’ll set up a system to handle all the responses we get from Apple, such as successful or declined. Once a payment is successful, we provide the user with the purchase. We are going to make a simple Collection View with custom cells and a purchase restore button. That will recover all your purchases if you ever delete the app or get a new device.

Your IAP iOS App (Figure 5.1.0) will have a UICollectionView, five UICollectionCells and one UIButton.

Figure 5.1.0

0.PNG

Creating an Xcode Project

Open Xcode and, from the File menu, select New and then New Project.... A new workspace window will appear, and a sheet will slide from its toolbar with several application templates to choose from. On the left-hand side, select Application from the iOS section. From the choices that appear, select Single View Application and press the Next button (Figure   5.1.1). (Apple changes these templates and their names often.)

Figure 5.1.1 1.png

Under Product Name enter the name of your project, I named mine InAppPurchases. Then press Next. (Figure 5.1.2)

Figure 5.1.2 2.png

Okay, so we are going to get straight to the purchase screen. Click on Main.Storyboard and lets place a UICollectionView on it. Let’s leave some space on the bottom for our restore purchase button.
Now pin it to the margins 0 to the top, 0 left and right and 40 from the bottom. (Figure 5.1.3)

Figure 5.1.3 3.png

Because this is not a tutorial on how to set up a UICollectionView or constraints, I’m going to go through it pretty quickly and just explain what we did.

Figure 5.1.4 4.png

Okay, so I made the cell 160 x 160 points big. I added a UIView to the cell and constrained it to the margins (so I can round the corners and add a shadow later on to make it prettier).

Second,I added a UIImage view and a label inside the view that was just added and pinned them 8 points all the way around and 4 points from the label (we can always adjust these as needed once we have real images in them). Lastly, I added the restore button under the Collection View and pinned it to the corner.

Lets add the assets now.
I added Arcade Time Background, Arcade-1, Arcade-2, Arcade-3, Arcade-4, and Bear-1. (Figure 5.1.5)

Figure 5.1.5 5.png

We can now add a image placeholder for the cell we created and also set up a UIImageView for a background.

First select the UIImageView inside the cell. Then select the Attributes inspector, where it says Image select the dropdown menu and select Arcade-1. Then for Content Mode select the dropdown menu and select Aspect Fit. (Figure 5.1.6)

Figure 5.1.6 6.png

Drag a UIImageView from the Object Library to the left side right above our Collection View, this will insure that the background image will be behind the Collection View. Once its there and still selected lets pin it right away. Uncheck Constrain to margins and lets put -20 from the top, and 0 from left, right and bottom. (Figure 5.1.7)

Figure 5.1.7 7.png

The Collection View needs to be clear so we can see our background when we set a image to it. Select your Collection View on the left side. Then select the Attributes inspector on the right and set the background color to clear. (Figure 5.1.8)

Figure 5.1.8 8.png

Select the UIImageView on the left side that we placed above our Collection View. Select Attributes inspector again and where it says Image, select the dropdown menu and select Arcade Time Background. Then for Content Mode select the dropdown menu and select Scale To Fill. (Figure 5.1.9)

Figure 5.1.9

9.png

Our Storyboard should now resemble something like this. We will get our Collection View functioning so we can see it in the simulator before we beautify it like Figure 5.1.0.

Figure 5.1.10

10.png

Now, before we can get the UICollectionView all set up and have the ViewController inherit from the UICollectionView Data source and delegate, we need to make a UICollectionViewCell file to customize the cell. Right click the InAppPurchases folder and select New file. Make sure you select iOS, then select Cocoa Touch Class and click Next.

Figure 5.1.11 11.png

Make sure the subclass selected is UICollectionViewCell and name the class PurchaseCell, click Next.
Make sure your project is selected on the next screen and click Create. We'll come back to this file later to add more to it, this is good for now. (Figure 5.1.12)

Figure 5.1.12 12.png

Now, you need to click on the Identity inspector on the right side and select the PurchaseCell class for the Collection View cell. (Figure 5.1.13)

Figure 5.1.13 13.png

We need to give this cell a reusable identifier as well. Click on the Attributes inspector and type purchaseCell in the identifier text field. (Figure 5.1.14)

Figure 5.1.14

14.png

We are now ready to set up our ViewController and connect all of our IBOutlets. Let’s add the UICollectionViewDelegate and UICollectionViewDataSource to our ViewController. We also need to set the delegate and data source and add the required methods to allow the UICollectionView protocols to work.

These are numberOfItemsInSection, cellForItemAt indexPath, and didSelectItemAt indexPath. For sizing purposes so our Collection View cells look nice on all screen sizes, lets inherit from one more protocol, add UICollectionViewDelegateFlowLayout next to UICollectionViewDelagate and DataSource at the top. Then add one more method to our class called collectionViewLayout.

import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    @IBOutlet weak var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        //This is a temporary value, just to show us some cells
        return 6
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "purchaseCell", for: indexPath) as? PurchaseCell {
            return cell
        }else {
            return UICollectionViewCell()
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        //This is so the cells look good on any screen size
        return CGSize(width: self.collectionView.bounds.size.width/2 - 20, height: 160)
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        //This gets called when cell is tapped
        //We will add to this later
    }
}

Lets connect our Collection View IBOutlet to our Storyboard. Open Assistant Editor and click the + next to the IBOutlet and drag it to the Collection View. (Figure 5.1.15)

Figure 5.1.15 15.png

Now, that this is finished we can test and run our app. We should see 6 cells with our claw machine image. (Figure 5.1.16)

Figure 5.1.16

16.png

We need to configure the cell with data so we can change the image and also update the image and label. So first, at the top of ViewController.swift, right under the CollectionView IBOutlet; declare an array of strings called products with the proper purchase item names you want to use.

let products = ["tier1","tier2","tier3","tier4","tier5"]

Select the PurchaseCell.swift file we created earlier, it is now time to add a configuration method and IBOutlets. We need two IBOutlets. One we will call purchaseImage of type UIImageView and the second one is purchaseLbl of type UILabel. Lastly, lets create a function called configureCell and pass in a variable called imageName of type String for now.

We'll use a switch case and manually set the label name and image for now. We'll come back and refactor this code to work with our IAP stuff later.
This is enough to get everything set up and tested to make sure all our images work and look good.

import UIKit

class PurchaseCell: UICollectionViewCell {

    @IBOutlet weak var purchaseImage: UIImageView!
    @IBOutlet weak var purchaseLbl: UILabel!

    func configureCell(imageName: String){
        switch imageName {
        case "tier1":
            purchaseImage.image = UIImage(named: "Arcade-1")
            purchaseLbl.text = "$2,500"
            break
        case "tier2":
            purchaseImage.image = UIImage(named: "Arcade-2")
            purchaseLbl.text = "$5,000"
            break
        case "tier3":
            purchaseImage.image = UIImage(named: "Arcade-3")
            purchaseLbl.text = "$10,000"
            break
        case "tier4":
            purchaseImage.image = UIImage(named: "Arcade-4")
            purchaseLbl.text = "$25,000"
            break
        case "tier5":
            purchaseImage.image = UIImage(named: "Bear-1")
            purchaseLbl.text = "$50,000"
            break
        default:
            break
        }
    }
}

Time to connect those IBOutlets to our Storyboard. Open Assistant Editor and click the + next to the IBOutlet and drag purchaseLbl to the Label in the cell, and purchaseImage to the UIImageView. (Figure 5.1.17)

Figure 5.1.17 17.png

Alright, we are almost ready to test this to see if we have 5 different cells. We need to refactor our ViewController.swift file a little, so select that. Lets change the return 6 in the numberOfItemsInSection to the count of our products array.

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return products.count
    }

One more code addition, we have to call our new configureCell method we created in our cell view and pass in one of our String values in our products array.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "purchaseCell", for: indexPath) as? PurchaseCell {

            cell.configureCell(imageName: products[indexPath.row])

            return cell
        }else {
            return UICollectionViewCell()
        }
}

Okay, lets run this in our simulator and we should have something that looks like this. (Figure 5.1.18)

Figure 5.1.18

18.png

The cells are really boring and just don't look great.
Lets create a custom view that will round the corners and put a shadow around the edge. Right click the InAppPurchases folder and select new file -> select iOS, then select Cocoa touch class and click Next. Make sure the subclass selected is UIView and name the class CustomView. Click Next.

Make sure your project is selected on the next screen and click Create.
Add the code shown below to this new swift file.

import UIKit

class CustomView: UIView {

    override func awakeFromNib() {
        layer.cornerRadius = 2.0
        layer.shadowColor = UIColor(red: 157.0 / 255.0, green: 157.0 / 255.0, blue: 157.0 / 255.0, alpha: 0.5).cgColor
        layer.shadowOpacity = 0.8
        layer.shadowRadius = 5.0
        layer.shadowOffset = CGSize(width: 0.0, height: 2.0)

    }
}

We need to add another UIView to our cell, so lets go to the Storyboard and drag a UIView right above our view that contains our UIImageView and UILabel. (Figure 5.1.19)

Figure 5.1.19

19.png

Once thats there, select both the new view and the one that was there and pin the view to the leading, trailing, top and bottom edges of the view thats already there. (Figure 5.1.20)

Figure 5.1.20 20.png

Go to your Main.Storyboard and select the view we placed in the cell and go to your Attributes inspector and select CustomView for the class. (Figure 5.1.21)

Figure 5.1.21 21.png

Select the CustomView we just placed in the cell and go to your Attributes inspector and change the alpha to 0.6. (Figure 5.1.22)

Figure 5.1.22 22.png

Select View that contains our UIImageView and UILabel and set the background to clear. (Figure 5.1.23)

Figure 5.1.23 23.png

Lastly, select the Restore button and change the font to anything you like and make sure you change the background color to clear. (Figure 5.1.24)

Figure 5.1.24 24.png

Now build and run your project and your cells will have slightly rounded corners and a little bit of a shadow (it is hard to see since the background is so dark). It should look just like figure 5.1.0 at the beginning of this section.

Time to setup In App Purchases!

First, we need to go to our iTunes Connect page and Click on My Apps. Then click the + symbol on the left side and select New App. (Figure 5.1.25 & 5.1.26)

Figure 5.1.25 25.png

Figure 5.1.26 26.png

We then need to create a bundle ID for our app if you haven’t already done so. Just click on the developer portal link. (Figure 5.1.27)

Figure 5.1.27

27.png

On the left side of the page under Identifiers select the App IDs link. Now you just need to name your app, in our case I named it InAppPurchases. Then create a bundle identifier, ours is com.devslopes.inAppPurchaseExample.

This should use your own custom domain, it can only be used once. Click continue, you will notice that in-app purchases is selected by default. Once you are on the next page just click register. (Figure 5.1.28 & Figure 5.1.29)

Figure 5.1.28 28.png

Figure 5.1.29

29.png

Now we can go back and refresh our iTunes connect account and our bundle ID should be available.

Once you see your bundle ID, fill everything else out.
(note: the name of the app needs to be unique and if it’s already taken you’ll have to rename it). Click Create. (Figure 5.1.30)

Figure 5.1.30

30.png

Select Features at the top and then press the + next to In-App Purchases to begin adding them. (Figure 5.1.31)

Figure 5.1.31 31.png

Select non-consumable for the type for this exercise. If you’d like to know what the other types are, the descriptions are very clear.
We are working with non-consumables because we can demonstrate the restore purchases feature this way. (Figure 5.1.32)

Figure 5.1.32 32.png

We now need to name this in-app purchase and give it a unique product ID and select a pricing tier.

I wont be going through this process for every tier (5), so for the other items we'll have in our app, I will be naming them tier1 through tier5 and copying the pricing tier to match their name. (Figure 5.1.33)

Figure 5.1.33 33.png

We need to have a language selected. For now, just add English and name the item whatever you'd like with a short description. Then press save. (Figure 5.1.34)

Figure 5.1.34 34.png

Now click save to finish entering the first item, and repeat this for tiers 2 through 5. (Figure 5.1.35)

Figure 5.1.35 35.png

When you finish adding all 5 items, your In-App Purchases should look like this. (Figure 5.1.36)

Figure 5.1.36 36.png

Request Available Products

Now that all of our products are entered into iTunes connect, we now need to request our products from Apple to make sure they are available to buy.
First, we need to import StoreKit in our class and also inherit SKProductsRequestDelegate.

Once we do that, we need to create a requestProducts function.
Inside this function we need a Set of strings with the bundle ID’s for all the products we added to iTunes Connect and then do a SKProductsRequest and wait for a response and hope we’re told they’re ready.

import UIKit
import StoreKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, SKProductsRequestDelegate {

    @IBOutlet weak var collectionView: UICollectionView!

    var products = ["tier1","tier2","tier3","tier4","tier5"]

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
        requestProducts()
    }

    func requestProducts() {
        let ids: Set<String> = ["com.devslopes.InAppPurchases.tier1","com.devslopes.InAppPurchases.tier2","com.devslopes.InAppPurchases.tier3","com.devslopes.InAppPurchases.tier4","com.devslopes.InAppPurchases.tier5"]
        let productsRequest = SKProductsRequest(productIdentifiers: ids)
        productsRequest.delegate = self
        productsRequest.start()
    }

Above, you see that we set the SKProductsRequest delegate.
Now we need to add a required function called productsRequest didReceive response. This is where we know if our products are available to sell or not.

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        print("Products ready: \(response.products.count)")
        print("Products not ready: \(response.invalidProductIdentifiers.count)")
}

Build and run your project. If your products ready doesn’t show all the items you’ve added, make sure your bundle ID’s on your project match the ones on iTunes Connect.

Okay, now that we are receiving products ready to sell from the App Store, let’s create a new array of SKProduct type. Replace the products array.

var products = [SKProduct]()

Then, inside our productRequest function we can set the products array equal to our response.products array with the items that are ready to sell.
Be sure to reload the Collection View after this and also update the array name.

swift
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        print("Products ready: \(response.products.count)")
        print("Products not ready: \(response.invalidProductIdentifiers.count)")
        products = response.products
        collectionView.reloadData()
        for product in response.products {
            print(product.productIdentifier)
        }
}

Alright, we are almost done with this. We need to inherit one more protocol in our class, called SKPaymentTransactionObserver. Place this next to the SKProductsRequestDelegate at top, next to the class name.

We will need to add the paymentQueue function to abide by this protocol and also set the observer.
Inside the paymentQueue function we will add a switch statement that takes care of all the cases that can be returned from Apple.

These are purchased, failed, restored, purchasing and deferred.

  • Purchased gets called when the item is successfully processed.
  • Failed is called if payment transaction fails.
  • Purchasing is called when the payment is being processed, this is called before the purchase is successful or not.
  • Restored is an option for non-consumable products like our products, that will restore all purchases automatically if you delete the app or get a new device.
  • Deferred means something happened and it will be tried again at a later time.
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                print("purchased")
                SKPaymentQueue.default().finishTransaction(transaction)
                break
            case .failed:
                print("failed")
                let errorMsg: String! = transaction.error?.localizedDescription
                showErrorAlert(title: "Oops! Something went wrong.", msg: "Unable to make purchase.  Reason: \(errorMsg).")
                SKPaymentQueue.default().finishTransaction(transaction)
                break
            case .restored:
                print("restored")
                showErrorAlert(title: "Purchases Restored.", msg: "Your purchases have been restored.")
                SKPaymentQueue.default().finishTransaction(transaction)
                break
            case .purchasing:
                print("purchasing")
                break
            case .deferred:
                print("deferred")
                break
            }
        }
    }

Also, add a UIAlert function that can be called so the user knows what is happening.

func showErrorAlert(title: String, msg: String) {
        let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert)
        let action = UIAlertAction(title: "Ok", style: .default, handler: nil)
        alert.addAction(action)
        present(alert, animated: true, completion:nil)
}

We need to update our configureCell method in our PurchaseCell.swift file. The image name we pass in is going to be the Product ID we created for our IAP's in iTunes Connect. We'll also want to pass in the price, because we can now get this from Apple and this allows us to use the correct price associated with the tier that was selected.

func configureCell(imageName: String, price: String){
        switch imageName {
        case "com.devslopes.InAppPurchases.tier1":
            purchaseImage.image = UIImage(named: "Arcade-1")
            purchaseLbl.text = price
            break
        case "com.devslopes.InAppPurchases.tier2":
            purchaseImage.image = UIImage(named: "Arcade-2")
            purchaseLbl.text = price
            break
        case "com.devslopes.InAppPurchases.tier3":
            purchaseImage.image = UIImage(named: "Arcade-3")
            purchaseLbl.text = price
            break
        case "com.devslopes.InAppPurchases.tier4":
            purchaseImage.image = UIImage(named: "Arcade-4")
            purchaseLbl.text = price
            break
        case "com.devslopes.InAppPurchases.tier5":
            purchaseImage.image = UIImage(named: "Bear-1")
            purchaseLbl.text = price
            break
        default:
            break
        }
}

Okay, so because we added another parameter price of type String to our configureCell method we'll need to get our price from our products array. We are going to format the number to currency and also use a nifty built in tool that will change the currency displayed to whatever country the user is in. Then we'll convert it to a String and send it into our configureCell method.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        var cellPrice = ""
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "purchaseCell", for: indexPath) as? PurchaseCell {
            let product = products[indexPath.row]
            let formatter = NumberFormatter()
            formatter.numberStyle = NumberFormatter.Style.currency
            formatter.locale = product.priceLocale
            if let price = formatter.string(from: product.price){
                cellPrice = "\(price)"
            }

            cell.configureCell(imageName: products[indexPath.row].productIdentifier, price: cellPrice)
            return cell
        }else {
        return UICollectionViewCell()
        }
}

Let’s make our Collection View cells selectable now, we’ll need to add an observer here, and also send the payment to the queue.

swift
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        SKPaymentQueue.default().add(self)
        let payment = SKMutablePayment(product: products[indexPath.row])
        payment.simulatesAskToBuyInSandbox = true
        SKPaymentQueue.default().add(payment)
}

One thing you need to remember is that we can’t test in-app purchases without using our device. You will also need to create a Sandbox tester in your iTunes Connect account. This cannot be your apple ID you already use. (Figure 5.1.37)

Figure 5.1.37 37.png

Select Users and Roles and then select Sandbox Testers and add a tester. Next, you need to log out of your apple ID in your General settings on your iPhone. Do not log in to your Sandbox account here, you will need to do this from inside the app when it prompts you. You are now ready to test your in-app purchases! (Figure 5.1.38)

Figure 5.1.38 38.png

One last thing to do is connect an IBAction to your restore button and add one line of code to it and that's it! All your purchases will be restored.

    @IBAction func restoreBtnPressed(_ sender: AnyObject) {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

Congratulations!

That’s all you need to do, to set up in-app purchases. Obviously, wherever I printed purchase and called a UIAlert view, you can add more code to maintain and change your data whether you are using Firebase or core data to keep track of purchases.

Wrapping up

That is all it takes to make in-app purchases. We covered a lot in this section. First we learned how to navigate through iTunes Connect and register our app ID for in-app purchases. Then we got multiple tiers set up and connected the app store with our app. Lastly, we learned how to create a sandbox user to test our in-app purchases so we don't actually get charged in our testing phases.

Exercise

I am going to leave you with a final challenge. Implement in-app purchases into one of your apps. Make it possible to track users' purchases and start making money with your apps!