Let’s go to the Main.storyboard and add a hidden activity indicator. We want to have an outlet for it:
When the buy button is tapped (in the buyPressed: method) we want to hide the buy button and show the activity indicator. Just call this method when the buy button is pressed:
func updateUIForPurchaseInProgress(inProgress: Bool){ buyButton.hidden = inProgress if inProgress { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } }
When we receive the SKPaymentTransactionStatePurchasing or the SKPaymentTransactionStateDeferred we are not really doing anything new. We can post a notification and update the UI (make sure the buy button is hidden and the activity indicator is showing), but it shouldn’t be visible to the user. The implementation of the showTransactionAsInProgress: would look something like this:
func showTransactionAsInProgress(deferred: Bool) { NSNotificationCenter.defaultCenter().postNotificationName(IAPTransactionInProgress, object: deferred) }
For the SKPaymentTransactionStateFailed we want to display an alert to inform the user that the payment was unsuccessful. We’ll also use a notification and we’ll set the error as the object for the notification:
func failedTransaction(transaction: SKPaymentTransaction) { NSNotificationCenter.defaultCenter().postNotificationName(IAPTransactionFailed, object: transaction.error) SKPaymentQueue.defaultQueue().finishTransaction(transaction) }
We also need to finish the transaction. If the user wants to try again then a new transaction will be initiated. Unfinished transactions remain in the queue until they’re finished and the transaction queue observer is called every time the app is launched. After you finish the transaction you can’t take any more actions on that transaction. In our case it’s ok to finish the transaction now, we know it failed, took some action and that’s it. However, if the transaction is successful and you want to start downloading content then you’re still using the transaction and calling finish should happen after the download is complete. So in our next method completeTransaction: we are going to send a notification to update the UI and this is where we will start downloading the photos for the product that was purchased:
func completeTransaction(transaction: SKPaymentTransaction) { NSNotificationCenter.defaultCenter().postNotificationName(IAPTransactionComplete, object: transaction) //will add code to start downloading photos }
We need to add the string definitions for each notification in order for our code to compile. Add the definitions outside the class:
let IAPTransactionInProgress = "IAPTransactionInProgress" let IAPTransactionFailed = "IAPTransactionFailed" let IAPTransactionComplete = "IAPTransactionComplete"
Now let’s go to the DetailViewController and add some behaviour for the notifications that we are being sent. First we need to sign up to receive notifications:
func signUpForNotifications() { NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedPurchaseInProgressNotification:", name: IAPTransactionInProgress, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedPurchaseFailedNotification:", name: IAPTransactionFailed, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "receivedPurchaseCompletedNotification:", name: IAPTransactionComplete, object: nil) }
You might have noticed that we haven’t used @selector for the name of the methods called when a notification is received. This is because Swift doesn’t use selectors. When interacting with Objective-C APIs that take selectors, you can use a string to represent one. Because our methods also take a parameter we’ve added the “:” symbol. You can read more about this in the section about “Interacting with Objective-C APIs” from Apple’s “Using Swift with Cocoa and Objective-C” book. Call this method from viewDidLoad:
We know that when working with NSNotification we need to have pairs of calls to addObserver and removeObserver. The method viewDidUnload: had been deprecated since iOS 6.0 and if you try to use dealloc in Swift you’ll see something like this:
If dealloc doesn’t work, where do we remove observers for notifications? In Swift you can use the deinit method. According to the Swift Programming Language book:
A deinitializer is called immediately before a class instance is deallocated. You write deinitializers with the deinit keyword, similar to how initializers are written with the init keyword. Deinitializers are only available on class type.
This sounds like what we need. So let’s implement this deinit method:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
The method receivePurchasedFailedNotification: will update the UI to show that there is no transaction in progress and will display an alert to inform the user that the payment request was unsuccessful. It will use the localisedDescription of the error message.
func receivedPurchaseFailedNotification(notification: NSNotification) { updateUIForPurchaseInProgress(false) let error = notification.object as 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 method receivePurchaseCompletedNotification: will also update the UI to stop showing transaction in progress. This method is incomplete at this time. We want to add a progress view to show the user the download status for the product that was bought. We are going to do that in the next part of this tutorial.
func receivedPurchaseCompletedNotification(notification: NSNotification){ updateUIForPurchaseInProgress(false) //show indicator that download is in progress }
Testing In-App Purchase
We now have enough code in place to at least have the payment transaction work. So how do we test this? You don’t want to pay every time you want to test in-app purchasing. Apple provides a testing environment called “Sandbox” that we can use to see that everything is working. We need to go to iTunes Connect and setup a test account. In iTunes Connect go to “Users and Roles” and then to “Sandbox Testers”
Create a new user account. Apple recommends that you use your device to test In-App Purchase. If you want to use the test account then sign out of the App Store in Settings. After signing out when you tap on the buy button you will be prompted to sign in to the App Store.
Use the test account and make sure you see “[Environment: Sandbox]”:
If the text “[Environment: Sandbox]” doesn’t appear it means that you’re using a production environment. Make sure you’re using a development-signed build of your app. Also, don’t use your test account to sign in to the production environment. It will become invalid and you will have to create a new one.
In the next part of this tutorial we are going to see how to download content hosted on Apple’s server. You can find the full project on Github. If you have any questions feel free to drop a comment.
Pingback: How to download content hosted on Apple's server | Mastering iOS