In the first part of this tutorial about In-App Purchase we have seen how to setup an app and configure everything in iTunes Connect. This second part assumes that you have already at least one product configured in iTunes Connect and its status is “Ready to Submit”. We’ll create an app that retrieves all the available IAP products from iTunes Connect and displays them in a table view. When you select a product you can see a details page with a description of the product. In order to do this we will use the Store Kit framework. So let’s start a new Master-Detail Application project.
Remember that in the first part of this tutorial we have already chosen a name and a bundle for this app so make sure that this is the one you’re using for this new project:
We’ll choose Swift as the language for this tutorial, but you can follow the same steps and have it working in Objective C too. The API for the framework is the same in Swift and Objective C, only the syntax will be different. After creating the project we want to go to the “Targets” section and see what capabilities are enabled for this app.
All capabilities are off for our app, so we need to indicate that we want to use In-App Purchase. Turning this feature on will add the Store Kit framework to our project and add this capability to our app ID in the iOS developer centre.
Retrieving the list of products
We want to retrieve the product or products that we have configured in iTunes Connect and display them in a table view. In order to do this we’ll use the SKProductRequest class. To initialise an object of this class we need to give it a set of product identifiers. We could write all this code in MasterViewController.swift file, but I think it would be better to encapsulate it in a separate class. So let’s create a class called IAPHelper. This is where we are going to create the request, check the response and later add code to buy products.
Each product you configure to sell in iTunes Connect has a unique identifier and that identifier is what we need in order to be able to retrieve information about the product. You can choose to hard code the product identifiers in your code or to keep them in a plist. These two options are not very flexible and if you add a new product in iTunes Connect you need to re-submit your app for approval. The more flexible option is to request these identifiers from your own server. This way new products will be visible in the list of products retrieved from iTunes Connect without any code change. While this option is more flexible, it is an overkill when you don’t add new products often, so choose wisely. We’ll choose the simplest option for our example and hard code the product IDs in the init method of our IAPHelper file.
class IAPHelper: NSObject { let productIdentifiers: NSSet override init() { productIdentifiers = NSSet(objects: "com.masteringios.Photos.WinterPhotos", "com.masteringios.Photos.CatPhotos", "com.masteringios.Photos.DogPhotos") }
Let’s create a method to retrieve products:
var completionHandler: ((Bool, [SKProduct]!) -> Void)! func requestProductsWithCompletionHandler(completionHandler:(Bool, [SKProduct]!) -> Void){ self.completionHandler = completionHandler let productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) productsRequest.delegate = self productsRequest.start() }
Let’s go through each line of code and understand what it does. Our method has one parameter, a completion handler that needs to be called when we have received a response. We store this handler to use it when the time is right.
Next we create a SKProductsRequest object using the product identifiers for the products setup in iTunes Connect. The request needs to have a delegate, so we’ll make this class the delegate as seen on the next row. You’ll need to indicate that the class conforms to the SKProductsRequestDelegate protocol in its definition. You’ll also need to import StoreKit.
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) { println("product count \(response.products.count)") println("invalid product IDs \(response.invalidProductIdentifiers)") if response.products.count > 0 { var products: [SKProduct] = [] for prod in response.products { if prod.isKindOfClass(SKProduct) { products.append(prod as SKProduct) } } completionHandler(true, products) } }
func request(request: SKRequest!, didFailWithError error: NSError!) { completionHandler(false, nil) }
Updating the UI
We have written all the code needed to retrieve the products list from the App Store, however nobody calls the methods we have written. In this section we’re going to address that and we’re going to make sure that our products are displayed in the table view.
First let’s remove the “Edit” button and the “+” button that came with the master detail app template. You can find the code in the viewDidLoad method of MasterViewController. You can also remove the insertNewObject method.
In viewDidLoad we want to make the request for products. For this we need to declare and create an IAPHelper object. This will be at class level and we can make it a constant as we don’t change it at any point:
let helper = IAPHelper()
Then in viewDidLoad: call the method like this:
self.helper.requestProductsWithCompletionHandler({ (success, products) -> Void in //completion handler implementation goes here })
We’re using a closure for the completion handler parameter and we need to provide the code for what should happen both for successful request and in case of an error. If we receive the list of products then we want to display it in the table view. The template already provided a mutable array of objects that acts like the data source for the table view, so we’re going to update it when we get back products. In case of an error we are going to show an alert view saying something went wrong. You should consider whether this is appropriate for a real life application, it might annoy the users if the app is used offline a lot. This is how the full request code looks like:
helper.requestProductsWithCompletionHandler({ (success, products) -> Void in if success { self.objects.addObjectsFromArray(products) self.tableView.reloadData() } else { let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) self.presentViewController(alert, animated: true, completion: nil) } })
There is one more thing we need to fix before we can run the app. The template was assuming that the objects are of type NSDate because that’s what the “+” button was adding to the array. However we know we have objects of type SKProduct. In tableView: cellForRowAtIndexPath: method make the following changes:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell let product = objects[indexPath.row] as SKProduct cell.textLabel!.text = product.localizedTitle return cell }
Ok, let’s run our app and see the results!
Debugging tips: If you don’t see the list of products first check if you get back invalid IDs. If so, make sure that the spelling is right (the IDs are case sensitive, so double check that). Also check that the status for the products configured in iTunes Connect is “Ready to Submit”.
The app is now displaying the list of products, however if you select one the app will crash. Let’s fix this quickly so we can leave the app in a stable state for the next step.
The app is crashing because when we setup the detail view controller we are still assuming the objects array contains NSDate. We’ll fix this by going to the prepareForSegue: method and replacing NSDate with SKProduct:
let object = objects[indexPath.row] as SKProduct
We want the detail view controller to have as title the name of the product, so we also add this line of code:
controller.title = object.localizedTitle
func configureView() { if let detail: AnyObject = self.detailItem { if let label = self.detailDescriptionLabel { label.text = detail.localizedDescription } } }
Pingback: In-App Purchase – How to implement the payment transaction | Mastering iOS
Hi, I keep getting this error: Type: ‘IAPPHelper’ does not conform to protocol ‘SKProductsRequestDelegate’ – What should I do?
Best regards,
Avi T.
You need to implement this method:
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!)
Download the latest version of my project from Github and see how it is implemented. That is the only required method in that protocol, so once you have that you should not see the error anymore.
Hi,
Once I’v successfully downloaded my products after purchase how do I present the downloaded content to the user?
The Content contains a string of text that can be placed inside of a UILabel.
Thanks in advanced,
Your tutorials are amazing.
AT.
Hi there,
Can you please let me know how I can fetch the list of In-app purchases of another app which is not mine?
To give you a context:
I’ve a page in my iOS app, which should show a list of In-app purchases of few apps. Out of these few apps, some are not owned by me.
This is purely just to show the list of IAPs and their price which needs to be fetched dynamically every time the page loads.
Kind regards,
Peter.
Hi Peter,
The only way I know of to retrieve a list of products is by supplying the product IDs. I don’t know of a way to retrieve the list of product IDs for an app. Sorry I can’t help.
SKProductResponse always return product count 0 🙁
why ?