I've implemented in app purchases into my shop scene for my game and have been having problems when changing from the shop scene to another scene it seems to crash the game and gives me this error
Thread 1: EXC_BAD_ACCESS (code=1, address=0x840f8010)
Or it gives me a multiple version of other errors such as:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x3f8)
it also sometimes gives me an error that changes lines in the subclass like so
When I comment out the code in the subclass that searches for the app purchase information or put the phone into airplane mode it works fine and the problem is gone.
I have a subclass of SKnode that gets the information on the purchasable items and then displays it through the use of SKLabels and sprite nodes to display a picture of the purchase in the shop as seen below:
class InAppPurchaseItems: SKNode, SKProductsRequestDelegate {
var shopItemNode = SKSpriteNode()
var itemPriceBackground = SKSpriteNode()
var shopItemLabel = SKLabelNode()
var shopItemTitleLabel = SKLabelNode()
var pressableNode = SKSpriteNode()
var itemPriceLabel = SKLabelNode()
var title: String = ""
var information: String = ""
var image: String = ""
var price:String = "X"
func createAppPurchaseItem(ID: String, purchaseImage: String, purchaseTitle:String) {
title = purchaseTitle
image = purchaseImage
createTheNode()
//let product = SKProduct()
let productID: NSSet = NSSet(objects: ID) //"RedShield.Astrum.Purchase", "DoubleCoin.Astrum.Purchase")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self as? SKProductsRequestDelegate
request.start()
}
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request...")
let myProduct = response.products
for product in myProduct {
print("product added")
if product.productIdentifier == "RedShield.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
information = product.localizedDescription
} else if product.productIdentifier == "DoubleCoin.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
information = product.localizedDescription
} else if product.productIdentifier == "1500Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirOne")
} else if product.productIdentifier == "7500Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirTwo")
} else if product.productIdentifier == "14000Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirThree")
} else if product.productIdentifier == "28000Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirFour")
} else if product.productIdentifier == "65000Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirFive")
} else if product.productIdentifier == "128000Stars.Astrum.Purchase" {
price = priceStringForProduct(item: product)!
shopItemNode.texture = SKTexture(imageNamed: "BuyStarsTeirSix")
}
createShopLabels()
}
}
func priceStringForProduct(item: SKProduct) -> String? {
let price = item.price
if price == 0 {
return "GET" //or whatever you like
} else {
let numberFormatter = NumberFormatter()
let locale = item.priceLocale
numberFormatter.numberStyle = .currency
numberFormatter.locale = locale
return numberFormatter.string(from: price)
}
}
func createTheNode() {
let tex:SKTexture = SKTexture(imageNamed: image)
shopItemNode = SKSpriteNode(texture: tex, color: SKColor.black, size: CGSize(width: 85, height: 85)) //frame.maxX / 20, height: frame.maxY / 20))
shopItemNode.zPosition = -10
shopItemNode.position = CGPoint(x: 0, y: 35)
self.addChild(shopItemNode)
self.name = "ShopItem"
self.zPosition = -11
shopItemTitleLabel = SKLabelNode(fontNamed: "Avenir-Black")
shopItemTitleLabel.fontColor = UIColor.black;
shopItemTitleLabel.fontSize = 15 //self.frame.maxY/30
shopItemTitleLabel.position = CGPoint (x: 0, y: -30)
shopItemTitleLabel.text = "\(title)"
shopItemTitleLabel.zPosition = -9
self.addChild(shopItemTitleLabel)
itemPriceBackground = SKSpriteNode(texture: SKTexture(imageNamed: "PriceShopBackground"), color: .clear, size: CGSize(width: 80, height: 30)) //SKSpriteNode(color: SKColor.black, size: CGSize(width: 65, height: 20))
//itemPriceBackground.alpha = 0.4
itemPriceBackground.zPosition = -10
itemPriceBackground.position = CGPoint(x: 0, y: -54)
addChild(itemPriceBackground)
pressableNode = SKSpriteNode(texture: nil, color: .clear, size: CGSize(width: 100, height: 140))
pressableNode.zPosition = -7
pressableNode.position = CGPoint(x: 0, y: 0)
shopItemSprites.append(pressableNode)
addChild(pressableNode)
}
func createShopLabels() {
shopItemLabel = SKLabelNode(fontNamed: "Avenir-Black")
shopItemLabel.fontColor = UIColor.white;
shopItemLabel.fontSize = 15 //self.frame.maxY/30
shopItemLabel.position = CGPoint (x: 0, y: -60)
shopItemLabel.text = "\(price)"
shopItemLabel.zPosition = -9
addChild(shopItemLabel)
}
}
they're then displayed on the store scene with the following code:
let ShopItem = InAppPurchaseItems()
ShopItem.createAppPurchaseItem(ID: "DoubleCoin.Astrum.Purchase", purchaseImage: "2StarCoin", purchaseTitle: "+2 In Game Pickups")
ShopItem.position = CGPoint(x: self.frame.midX / 1.6, y: self.frame.midY * 0.8)
ShopItem.zPosition = 100
ShopItem.name = "Shp0"
moveableArea.addChild(ShopItem)
Shop's Main Class
The shops main class also has in app purchase code that is used for buying the product and also to be able to search for the product information just like in the subclass as seen below
class ShopItemMenu: SKScene, SKProductsRequestDelegate, SKPaymentTransactionObserver {
//Purchase Variables
var listOfProducts = [SKProduct]()
var p = SKProduct()
override func didMoveToView(to view: SKView) {
let ShopItem = InAppPurchaseItems()
ShopItem.createAppPurchaseItem(ID: "DoubleCoin.Astrum.Purchase", purchaseImage: "2StarCoin", purchaseTitle: "+2 In Game Pickups")
ShopItem.position = CGPoint(x: self.frame.midX / 1.6, y: self.frame.midY * 0.8)
ShopItem.zPosition = 100
ShopItem.name = "Shp0"
moveableArea.addChild(ShopItem)
}
//This function allows for a product to be bought buy the user and starts the proccess for purchasing
func appPurchaseBuying(appPurchaseID:String) {
for product in listOfProducts {
let prodID = product.productIdentifier
if(prodID == appPurchaseID) {
p = product
buyProduct()
}
}
}
//This Function restores all previously purchased Items (use this for the restor button.
func restorePurchasesOfItems() {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
//This function checks if they can make payments and then loads the product ids from a harcoded set. (use this to start when the scene starts)
func checkCanMakePayment() {
if (SKPaymentQueue.canMakePayments()) {
print("can make payments...")
let productID: NSSet = NSSet(objects: "RedShield.Astrum.Purchase", "DoubleCoin.Astrum.Purchase")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
let alert = UIAlertController(title: "In-App Purchases Not Enabled", message: "Please enable In App Purchases in Settings", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Settings", style: UIAlertActionStyle.default, handler: { alertAction in
alert.dismiss(animated: true, completion: nil)
let url: NSURL? = NSURL(string: UIApplicationOpenSettingsURLString)
if url != nil
{
UIApplication.shared.openURL(url! as URL)
}
}))
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: { alertAction in
alert.dismiss(animated: true, completion: nil)
}))
if let vc = self.scene?.view?.window?.rootViewController {
vc.present(alert, animated: true, completion: nil)
}
}
}
//This allows the user to buy the product with a product idetifier given by the variable "p"
func buyProduct() {
print("buying " + p.productIdentifier)
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
//This Function gets all the avaliable products from apple and puts them into the product Array called listOfProducts
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
/* print("product request...")
let myProduct = response.products
for product in myProduct {
print("product added")
if product.productIdentifier == "RedShield.Astrum.Purchase" {
shieldPurchasePrice = priceStringForProduct(item: product)!
} else if product.productIdentifier == "DoubleCoin.Astrum.Purchase" {
DoubleCoinPurchasePrice = priceStringForProduct(item: product)!
}
/*print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
*/
listOfProducts.append(product)
}*/
}
func priceStringForProduct(item: SKProduct) -> String? {
let price = item.price
if price == 0 {
return "GET" //or whatever you like
} else {
let numberFormatter = NumberFormatter()
let locale = item.priceLocale
numberFormatter.numberStyle = .currency
numberFormatter.locale = locale
return numberFormatter.string(from: price)
}
}
//This Function restores all the already purchased products so that things can be restored such as shield
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("restoring all...")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "RedShield.Astrum.Purchase":
isRedShieldPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isRedShieldPurchaseOn, forKey: "shieldPurchase")
print("finished restoring this purchase")
case "DoubleCoin.Astrum.Purchase":
isCoinPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isCoinPurchaseOn, forKey: "doubleCoinPurchase")
print("finished restoring this purchase")
default:
print("IAP not found")
}
}
alert(title: "Restored", msg: "Purchases were restored")
}
//This Function is run when the user makes a purchase and checks the state of the purchase to make sure it works
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("adding payment...")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState {
case .purchased:
print("buying ok, Unlocking purchase...")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "RedShield.Astrum.Purchase":
isRedShieldPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isRedShieldPurchaseOn, forKey: "shieldPurchase")
print("unlocked Purchase")
case "DoubleCoin.Astrum.Purchase":
isCoinPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isCoinPurchaseOn, forKey: "doubleCoinPurchase")
print("unlocked Purchase")
case "SOME IN APP PURCHASE ID HERE":
print("unlocked Purchase")
default:
print("IAP Not found")
}
queue.finishTransaction(trans)
case .failed:
print("error with payment...")
queue.finishTransaction(trans)
default:
print("Default")
}
}
}
Am I going about doing this the right way or is there a better way to do this and how can I fix the crashing issue i'm having?
EDIT
EDIT 2
EDIT 3
EDIT 4
import Foundation
import SpriteKit
import StoreKit
class PurchaseService {
static let session = PurchaseService()
var products = [SKProduct]()
var p = SKProduct()
//This function allows for a product to be bought buy the user and starts the proccess for purchasing
func appPurchaseBuying(appPurchaseID:String) {
for product in products {
let prodID = product.productIdentifier
if(prodID == appPurchaseID) {
p = product
buyProduct()
}
}
}
//This Function restores all previously purchased Items (use this for the restor button.
func restorePurchasesOfItems() {
//SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
//This function checks if they can make payments and then loads the product ids from a harcoded set. (use this to start when the scene starts)
func checkCanMakePayment() {
if (SKPaymentQueue.canMakePayments()) {
print("can make payments...")
let productID: NSSet = NSSet(objects: "RedShield.Astrum.Purchase", "DoubleCoin.Astrum.Purchase")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
//request.delegate = self
request.start()
} else {
let alert = UIAlertController(title: "In-App Purchases Not Enabled", message: "Please enable In App Purchases in Settings", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Settings", style: UIAlertActionStyle.default, handler: { alertAction in
alert.dismiss(animated: true, completion: nil)
let url: NSURL? = NSURL(string: UIApplicationOpenSettingsURLString)
if url != nil
{
UIApplication.shared.openURL(url! as URL)
}
}))
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: { alertAction in
alert.dismiss(animated: true, completion: nil)
}))
}
}
//This allows the user to buy the product with a product idetifier given by the variable "p"
func buyProduct() {
print("buying " + p.productIdentifier)
let pay = SKPayment(product: p)
//SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
//This Function gets all the avaliable products from apple and puts them into the product Array called listOfProducts
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request...")
let myProduct = response.products
for product in myProduct {
print("product added")
products.append(product)
}
}
func priceStringForProduct(item: SKProduct) -> String? {
let price = item.price
if price == 0 {
return "GET" //or whatever you like
} else {
let numberFormatter = NumberFormatter()
let locale = item.priceLocale
numberFormatter.numberStyle = .currency
numberFormatter.locale = locale
return numberFormatter.string(from: price)
}
}
//This Function restores all the already purchased products so that things can be restored such as shield
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("restoring all...")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "RedShield.Astrum.Purchase":
isRedShieldPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isRedShieldPurchaseOn, forKey: "shieldPurchase")
print("finished restoring this purchase")
case "DoubleCoin.Astrum.Purchase":
isCoinPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isCoinPurchaseOn, forKey: "doubleCoinPurchase")
print("finished restoring this purchase")
default:
print("IAP not found")
}
}
//alert(title: "Restored", msg: "Purchases were restored")
}
//This Function is run when the user makes a purchase and checks the state of the purchase to make sure it works
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("adding payment...")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState {
case .purchased:
print("buying ok, Unlocking purchase...")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "RedShield.Astrum.Purchase":
isRedShieldPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isRedShieldPurchaseOn, forKey: "shieldPurchase")
print("unlocked Purchase")
case "DoubleCoin.Astrum.Purchase":
isCoinPurchaseOn = true
let defaults = UserDefaults.standard
defaults.set(isCoinPurchaseOn, forKey: "doubleCoinPurchase")
print("unlocked Purchase")
case "SOME IN APP PURCHASE ID HERE":
print("unlocked Purchase")
default:
print("IAP Not found")
}
queue.finishTransaction(trans)
case .failed:
print("error with payment...")
queue.finishTransaction(trans)
default:
print("Default")
}
}
}
}
Having all the StoreKit code in your game scene makes it more difficult to isolate the issue you're having. I'd suggest you make a new swift file let's call it PurchaseService with a static instance like this:
class PurchaseService {
static let session = PurchaseService()
var products = [SKProduct]()
// code
}
You can implement all your purchasing related logic here. I usually use a getPurchases function to load the available purchases from the store and call it from the application function of the AppDelegate.swift file. This ensures that your purchases get loaded very early and will be ready the first moment you need them (because you make a static instance you can refer to it any time you need to do a purchase through PurchaseService.session...)
To get the prices you can use a function that iterates through your products variable and checks the product id:
func price(for productID:String)->Double{
if products.count>0 {
for product in products {
if product.productIdentifier == productID {
return product.price.doubleValue
}
}
}
}
If you comply to the SKProductRequestDelegate protocol you don't need to conditionally cast self to it:
// unnecessary: request.delegate = self as? SKProductsRequestDelegate
request.delegate = self
Wondering if you made the productRequest method public because by the time the request returns the SKProductResponse object self is no longer available.
Regarding Objective-C code in your project: I see you might be using Firebase (from your console messages I'm inferring) and it has some objective-c bits and pieces.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With