I'm currently offering in-app-purchases in a Swift based iOS app. I have a Store class which does the heavy lifting and have a StoreViewController which manages the view for the store. Currently when you press a buy button the response takes quite a while for the store (over 10 seconds minimum). This means that you could press the button repeatedly or give up on the purchase. I would like to change it so that when you press a button it disables the buttons and displays a spinner or similar until the buy process is ready to continue. In the code that I have already what would be the preferred way/place to do this?
Store class:
import Foundation
import StoreKit
protocol ClassStoreDelegate: class {
func storeUpdateReceived(store: Store)
}
class Store: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
// Properties
weak var delegate: ClassStoreDelegate?
var list = [SKProduct]()
var p = SKProduct()
var localPrice: String?
var presentAlert = false
var alertTitle = "Alert"
var alertMessage = "Unfortunately something went wrong"
var purchaseSuccess = false
// Methods
// Calling the delegate method
func storeUpdate() {
delegate?.storeUpdateReceived(store: self)
}
// Buy the product
func buy(productId: String) {
var foundProduct = false
var counter = 0
while counter < list.count && !foundProduct {
if (list[counter].productIdentifier == productId){
p = list[counter]
foundProduct = true
}
counter = counter + 1
}
buyProduct()
}
func buyProduct() {
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
switch trans.transactionState {
case .purchased:
let prodID = p.productIdentifier
switch prodID {
case "volume2":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 1)
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
case "volume3":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 2)
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
case "volume4":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 3)
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
case "volume5":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 4)
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
case "all":
for i in 0...VolumeTableViewController.unlocked.count-1 {
VolumeTableViewController.saveUnlocked(volumeUnlocked: i)
}
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
default:
delegate?.storeUpdateReceived(store: self)
}
queue.finishTransaction(trans)
break
case .failed:
alertTitle = "Something went wrong"
alertMessage = "Unfortunately the purchase failed"
presentAlert = true
delegate?.storeUpdateReceived(store: self)
queue.finishTransaction(trans)
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
// Try to pop back after successful restore
purchaseSuccess = true
delegate?.storeUpdateReceived(store: self)
queue.finishTransaction(trans)
break
default:
break
}
}
}
// Restore products
func restore() {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
func getProducts() {
if(SKPaymentQueue.canMakePayments()) {
let productID: NSSet = NSSet(objects: "volume2", "volume3", "volume4", "volume5", "all")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
alertTitle = "Something went wrong"
alertMessage = "Your phone does not appear to be set up for making purchases"
presentAlert = true
delegate?.storeUpdateReceived(store: self)
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let myProduct = response.products
for product in myProduct {
list.append(product)
}
delegate?.storeUpdateReceived(store: self)
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
let transactionsArray = queue.transactions
if (transactionsArray.isEmpty) {
alertTitle = "Something went wrong"
alertMessage = "We weren't able to find any previous purchases for your account"
presentAlert = true
delegate?.storeUpdateReceived(store: self)
}
else {
for transaction in transactionsArray {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "volume2":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 1)
delegate?.storeUpdateReceived(store: self)
case "volume3":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 2)
delegate?.storeUpdateReceived(store: self)
case "volume4":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 3)
delegate?.storeUpdateReceived(store: self)
case "volume5":
VolumeTableViewController.saveUnlocked(volumeUnlocked: 4)
delegate?.storeUpdateReceived(store: self)
case "all":
for i in 0...VolumeTableViewController.unlocked.count-1 {
VolumeTableViewController.saveUnlocked(volumeUnlocked: i)
}
delegate?.storeUpdateReceived(store: self)
default:
alertTitle = "Something went wrong"
alertMessage = "We weren't able to find the correct product"
presentAlert = true
delegate?.storeUpdateReceived(store: self)
}
}
}
}
// Format the price and display
func formatPrice(price: NSDecimalNumber) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .currency
if let formattedPrice = formatter.string(from: price){
localPrice = (" \(formattedPrice)")
}
return localPrice!
}
}
StoreViewController class:
let store = Store() // Global store instance
import UIKit
class StoreViewController: UIViewController, ClassStoreDelegate {
// Properties
/* let alert = UIAlertController(title: "Something went wrong", message:
"Unfortunately, something went wrong with your request", preferredStyle: UIAlertControllerStyle.alert) */
// Actions
@IBAction func btn2(_ sender: UIButton) {
store.buy(productId: store.list[0].productIdentifier)
}
@IBAction func btn3(_ sender: UIButton) {
store.buy(productId: store.list[1].productIdentifier)
}
@IBAction func btn4(_ sender: UIButton) {
store.buy(productId: store.list[2].productIdentifier)
}
@IBAction func btn5(_ sender: UIButton) {
store.buy(productId: store.list[3].productIdentifier)
}
@IBAction func btnAll(_ sender: UIButton) {
store.buy(productId: store.list[4].productIdentifier)
}
@IBAction func btnRestore(_ sender: UIButton) {
store.restore()
}
// Outlets
@IBOutlet weak var btn2: UIButton!
@IBOutlet weak var btn3: UIButton!
@IBOutlet weak var btn4: UIButton!
@IBOutlet weak var btn5: UIButton!
@IBOutlet weak var btnAll: UIButton!
@IBOutlet weak var btnRestore: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Disable buttons until prices loaded
btn2.isEnabled = false
btn3.isEnabled = false
btn4.isEnabled = false
btn5.isEnabled = false
btnAll.isEnabled = false
btnRestore.isEnabled = false
store.delegate = self // bind the delegate like this?
// alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default)) // Set up the action for the alert button
// Get list of products for the store
self.navigationItem.title = "Store"
// update once the list of products is got from the store object
store.getProducts()
}
// Running the delegate update
func storeUpdateReceived(store: Store) {
if ((store.list.count) > 0) {
btnRestore.setTitle("Restore", for: .normal)
if store.list[0].productIdentifier == "all" {
btn2.setTitle(store.list[0].localizedTitle + " " + store.formatPrice(price: store.list[0].price), for: .normal)
}
if store.list[1].productIdentifier == "volume2" {
if VolumeTableViewController.unlocked[1] {
btn3.isHidden = true
} else {
btn3.setTitle(store.list[1].localizedTitle + " " + store.formatPrice(price: store.list[1].price), for: .normal)
}
}
if store.list[2].productIdentifier == "volume3" {
if VolumeTableViewController.unlocked[2] {
btn4.isHidden = true
} else {
btn4.setTitle(store.list[2].localizedTitle + " " + store.formatPrice(price: store.list[2].price), for: .normal)
}
}
if store.list[3].productIdentifier == "volume4" {
if VolumeTableViewController.unlocked[3] {
btn5.isHidden = true
} else {
btn5.setTitle(store.list[3].localizedTitle + " " + store.formatPrice(price: store.list[3].price), for: .normal)
}
}
if store.list[4].productIdentifier == "volume5" {
if VolumeTableViewController.unlocked[4] {
btnAll.isHidden = true
} else {
btnAll.setTitle(store.list[4].localizedTitle + " " + store.formatPrice(price: store.list[4].price), for: .normal)
}
}
// Now enable the buttons
btn2.isEnabled = true
btn3.isEnabled = true
btn4.isEnabled = true
btn5.isEnabled = true
btnAll.isEnabled = true
btnRestore.isEnabled = true
}
if store.purchaseSuccess {
performSegueToReturnBack()
store.purchaseSuccess = false
}
}
// method to go back when complete
func performSegueToReturnBack() {
if let nav = self.navigationController {
nav.popViewController(animated: true)
} else {
self.dismiss(animated: true, completion: nil)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I would like to change it so that when you press a button it disables the buttons
Then disable the buttons or hide the buy interface, in immediate response to the tap on the Buy button.
and displays a spinner or similar until the buy process is ready to continue
You can't. You have no way to know what's happening. The "buy process" is an interchange outside your app, between the user and the runtime. You are not informed of what's going on. If the user cancels the purchase, or if there is any problem with the user's password or store account, you won't get any event informing you of what's happened. So if you put up some special "waiting" interface, it might remain forever.
Basically, the title of your question reveals a deep misconception. You do not "wait". You just go on with the life of your app. The purchase process is asynchronous and it is out of process for your app. When and if the payment queue tells your app that something has happened, you respond; that's all.
(See Apple's guidance about this; as they point out, under some circumstances the message from the payment queue might not arrive for days! You'd look pretty silly "waiting" for that.)
So the correct procedure is what we said first. If the user taps Buy, disable or take down the purchase interface, and that's all.