'Why does App keeps crashing when I tap paid (IAP) because 'Index out of range' come with Memory and servers, too? (Swift)
I am not sure if someone in this StackOverflow has skills with Apple's Code to create IAP without an installed Cocoapod. (BTW: I am not fans of Cocoapod) I am here to show you what I find code that I has trouble with this situation for many hours.
First start here
import UIKit
import StoreKit
let nc = NotificationCenter.default
struct dataStoreMemory {
static var fromLocal: [Int] = UserDefaults.standard.object(forKey: "Already Purchased") as? [Int] ?? [0]
}
var models = [SKProduct]()
var askedForEpisode: Int? = nil
enum Product: String, CaseIterable {
case Episode2 = "com.CoreJezlleland.Episode2"
case Episode3 = "com.CoreJezlleland.Episode3"
case Episode4 = "com.CoreJezlleland.Episode4"
case Episode5 = "com.CoreJezlleland.Episode5"
case Episode6 = "com.CoreJezlleland.Episode6"
case Episode7 = "com.CoreJezlleland.Episode7"
case Episode8 = "com.CoreJezlleland.Episode8"
case Episode9 = "com.CoreJezlleland.Episodes9"
case Episode10 = "com.CoreJezlleland.Episode10"
}
extension EpisodesViewController {
func fetchingProducts() {
let request = SKProductsRequest(productIdentifiers: Set(Product.allCases.compactMap({ $0.rawValue })))
request.delegate = self
request.start()
}
This is only one What caused crashed and console keep tell me 'Index out of range' and 'Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 3 beyond bounds for empty NSArray'
func makePayment() {
if SKPaymentQueue.canMakePayments() {
print("before")
let payment = SKPayment(product: models[(askedForEpisode ?? 0)])
// MARK: Crashed
// askedForEpisode has crashed because 'Index out of range'
print("Crash")
fetchingProducts()
SKPaymentQueue.default().add(payment)
} else {
print("User unable to make payment")
}
}
(keep add code below)
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
models = response.products
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach({
switch $0.transactionState {
case .purchasing:
print("Payment in processing")
case .purchased:
print("Payment Purchased")
SKPaymentQueue.default().finishTransaction($0)
thankYouPayment()
dataStoreMemory.fromLocal.append(askedForEpisode!)
UserDefaults.standard.set(dataStoreMemory.fromLocal, forKey: "Already Purchased")
case .failed:
print("Payment Fail")
SKPaymentQueue.default().finishTransaction($0)
case .restored:
print("Payment Restored")
self.dismiss(animated: true)
thankYouPayment()
case .deferred:
print("Payment Deferred")
@unknown default:
print("Unknown Payment")
}
})
}
@objc func restoreTapped() {
present(restoreWait, animated: true, completion: {
SKPaymentQueue.default().restoreCompletedTransactions()
})
}
func thankYouPayment() {
restoreWait.dismiss(animated: true)
let ac = UIAlertController(title: "Thank You", message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Alright!", style: .default))
present(ac, animated: true)
nc.post(name: NSNotification.Name("Reload List"), object: nil)
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
restoreWait.dismiss(animated: true)
if queue.transactions.count == 0 {
let ac = UIAlertController(title: "Restore Error", message: "You do not have any restore purchases in your account.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
} else {
dataStoreMemory.fromLocal.removeAll()
for x in queue.transactions {
for num in 1...10 {
if x.transactionIdentifier == "com.CoreJezlleland.Episode\(num)" {
dataStoreMemory.fromLocal.append(num)
}
}
}
thankYouPayment()
}
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
restoreWait.dismiss(animated: true)
let ac = UIAlertController(title: "Restore Error", message: "There was a problem while restoring your purchased. We recommend you try again.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
}
func errorPayment() {
restoreWait.dismiss(animated: true)
let ac = UIAlertController(title: "Error Payment", message: "The payment was declined or something went wrong, so please try again.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
}
}
End of Code
Now I created this tableView.deselectRow in extension EpisodesViewController: UITableViewDataSource, UITableViewDelegate {}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch detectBypassTime() {
case .NoInternet:
// (Alert popup) Do something if there is no internet
print("Sorry, no internet.")
let imageView = UIImageView()
imageView.image = UIImage(named: "No Wifi")
// no wifi : wifi.slash
// wrong clock: Incorrect Clock
imageView.translatesAutoresizingMaskIntoConstraints = false
// Create the alert controller
let alertController = UIAlertController(title: "\n\n\(noWifiTtitle)", message: noWifiMessage, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: cancelText, style: UIAlertAction.Style.cancel) {
UIAlertAction in
print("Cancel Pressed")
}
// Add the actions
alertController.addAction(cancelAction)
alertController.view.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 10).isActive = true
imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
// Present the controller
self.present(alertController, animated: true, completion: nil)
case .BadTime:
// (Alert popup) Do something is iPhone or iPad time / date is incorrect.
print("Sorry, your device is incorrect time / date. Please fix it.")
let imageView = UIImageView()
imageView.image = UIImage(named: "Incorrect Clock")
// no wifi : wifi.slash
// wrong clock: Incorrect Clock
imageView.translatesAutoresizingMaskIntoConstraints = false
// Create the alert controller
let alertController = UIAlertController(title: "\n\n\(clockBlockTtile)", message: clockBlockMessage, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: cancelText, style: UIAlertAction.Style.cancel) {
UIAlertAction in
print("Cancel Pressed")
}
// Add the actions
alertController.addAction(cancelAction)
alertController.view.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 10).isActive = true
imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
// Present the controller
self.present(alertController, animated: true, completion: nil)
case .Passed:
// Move all didselect into here.
print("Go ahead, enjoy the story!")
}
if dataStoreMemory.fromLocal.contains(indexPath.row) {
accessStory(row: indexPath)
} else {
askedForEpisode = indexPath.row
makePayment()
}
}
I forgot to mention that 'switch detectBypassTime() {}' shows up from above to create from these codes in First View Controller to check if WiFi or Data is on to work Apple Pay in 'EpisodeViewController'
extension WelcomeViewController {
@objc func startTimeDetect() {
let checkTimeBypass = detectBypassTime()
switch checkTimeBypass {
case .NoInternet:
// Do something if there is no internet
print("Sorry, no internet.")
case .BadTime:
// Do something is iPhone or iPad time / date is incorrect.
print("Sorry, your device is incorrect time / date. Please fix it.")
case .Passed:
// Do something if time / date is normal.
print("Go ahead, enjoy the story!")
}
}
}
enum BypassTimeMode {
case NoInternet
case BadTime
case Passed
}
func detectBypassTime() -> BypassTimeMode {
let dateformatter = DateFormatter()
dateformatter.dateFormat = "yyyy-MM-dd hh:mm"
if timeServer() != nil {
let localTime = dateformatter.string(from: Date())
let serverTime = dateformatter.string(from: timeServer()!) // Few mins later it will cause itself app crashed.
if localTime == serverTime {
return .Passed
} else {
return .BadTime
}
} else {
return .NoInternet
}
}
func timeServer() -> Date? {
let url = URL(string: "http://worldtimeapi.org/api/ip")!
do {
var getTimeSince1970: Int!
let jsonData = try Data(contentsOf: url, options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(JSONTime.self,from: jsonData)
getTimeSince1970 = Int("\(jsonCatalogs!.unixtime)")
let getDate = Date(timeIntervalSince1970: TimeInterval(getTimeSince1970!))
return getDate
} catch {
return nil
}}
struct JSONTime: Decodable {
let unixtime: Int
}
Well, in that cases I used the codes for IAP and Memory that were created by Apple, Kinda hard to understand why it was hard to find a way to fix those issues with NSArray in making sure payment() stop crashing and pop alert for pay. Does someone know how to fix it?
I am trying to understand Apple's IAP codes that you may have recommended and not recommend to make solved with IAP issues. Thank you.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
