Run another function once transaction has been completed - swift

I am trying to run a function that calls my API to update user info, including the number of coins the user has after a transaction has been made. I can't figure out how to do that. Here's my code for the transaction:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
transactionState = .purchasing
print("Purchasing...")
case .purchased:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
print("Purchased.")
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
Task {
await postCoins(receipt: receiptString)
}
}
catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
transactionState = .purchased
queue.finishTransaction(transaction)
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .restored
case .failed, .deferred:
print("Payment queue error:")
print(String(describing: transaction.error))
queue.finishTransaction(transaction)
transactionState = .failed
default:
queue.finishTransaction(transaction)
}
}
}
This function is inside an SKProductsRequestDelegate
I tried making this function async by adding a completion handler and then using DispatchQueue.main.async to update the thread where the payment is made, but it throws me an error saying that does not conform the that particular protocol. It makes sense that it threw that error, but I don't know another workaround.

Related

Wait until part of the function completes to execute the function

I'm trying to fetch data and update core data based on the new updated API-Data.
I have this download function:
func download1(stock: String, completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.downloadQueue")
let downloadGroup = DispatchGroup()
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote)
downloadGroup.leave()
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
On the ContentView I try to implement an update function:
func updateAPI() {
for stock in depot.aktienKatArray {
download.download1(stock: stock.aKat_symbol ?? "") { _ in
//
}
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
PersistenceController.shared.saveContext()
}
My problem is that the for loop in the update function should only go on if the first part (download.download1) is finished with downloading the data from the API.
Don't wait! Never wait!
DispatchGroup is a good choice – however nowadays I highly recommend Swift Concurrency – but it's at the wrong place.
.enter() must be called inside the loop before the asynchronous task starts
.leave() must be called exactly once inside the completion handler of the asynchronous task (ensured by a defer statement)
I know this code won't work most likely, but I merged the two functions to the correct DispatchGroup workflow. I removed the custom queue because the NetworkManager is supposed to do its work on a custom background queue
func updateAPI() {
var internalQuotes = [Quote]()
let downloadGroup = DispatchGroup()
for stock in depot.aktienKatArray {
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { result in
defer { downloadGroup.leave() }
switch result {
case .failure(let err):
print(err)
case .success(let resp):
internalQuotes.append(resp.quote)
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
}
}
downloadGroup.notify(queue: .main) {
self.quotes.append(contentsOf: internalQuotes)
PersistenceController.shared.saveContext()
}
}

How can I get my app to wait for a permission request to complete?

My updated code is below, added a semaphore, and the app still blows through the AVCaptureDevice.authorizationStatus part and keeps running.
However, if I declare the semaphore with 0, then the first semaphore.wait() is successful, and the program freezes because the userAlert permission box never pops up.
So am having a tough time figuring out what the issue is here.
print ("going in...")
let semaphore = DispatchSemaphore(value: 1 )
DispatchQueue.global(qos: .userInitiated).async {
let mediaAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .audio)
switch mediaAuthorizationStatus {
case .denied:
print (".denied")
case .authorized:
print ("authorized")
case .restricted:
print ("restricted")
case .notDetermined:
print("Need to ask user")
semaphore.wait()
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { (granted: Bool) in
if granted {
semaphore.signal()
} else {
semaphore.signal()
}
})
#unknown default:
print("unknown")
}
print ("\(semaphore.debugDescription)")
}
semaphore.wait()
print ("and we're out")
Misusing DispatchQueue to force an asynchronous task to become synchronous is a very bad practice.
Either use a completion handler
func avAuthorization(completion : #escaping (Bool) -> Void)
{
let mediaType = AVMediaType.audio
let mediaAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
switch mediaAuthorizationStatus {
case .denied, .restricted: completion(false)
case .authorized: completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .audio) { granted in
completion(granted)
}
}
}
Or in Swift 5.5+ an async function
func avAuthorization() async -> Bool
{
let mediaType = AVMediaType.audio
let mediaAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
switch mediaAuthorizationStatus {
case .denied, .restricted: return false
case .authorized: return true
case .notDetermined: return await AVCaptureDevice.requestAccess(for: .audio)
}
}
I think you are already on the main thread and trying to DispatchQueue.main.sync from the Main thread. Check it like this.
if Thread.isMainThread {
// you are already on the main thread
} else {
DispatchQueue.main.sync {
// do stuff
}
}
without waiting for the iOs permission alert to pop up and complete
Great line, partially describing possible solution (imho).
AFAIK all UIAlertController-related routines are richly flavoured with completions. I'd try to put recording logic trigger inside the completion handler of method that presents the alert itself.

How can I abort the entire dispatch group operation upon a single failure without returning multiple failure completion handlers?

I use dispatch group in a loop in order to keep track an array of network requests so when they are all successfully completed then the successful completion handler is returned only once.
However, if a single failure occur, I want to abort the entire operation and return the failure completion handler only once. The problem I am facing is that all failure completion handlers are returned multiple times. Which is not what I want.
My code looks something like this.
class NetworkClient {
// ...
func fetchBlogPosts(completion: #escaping (Result<[BlogPost], NetworkClientError>) -> Void) {
let dispatchGroup = DispatchGroup()
var blogPosts = [BlogPost]()
for (index, value) in blogPostJSONURLs.enumerated() {
dispatchGroup.enter()
guard let jsonURL = URL(string: blogPostJSONURLs[index]) else {
dispatchGroup.leave()
completion(.failure(.invalidURL))
return
}
let dataTask = URLSession.shared.dataTask(with: jsonURL) { data, response, error in
if error != nil {
dispatchGroup.leave()
completion(.failure(.errorReturned))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
dispatchGroup.leave()
completion(.failure(.serverError))
return
}
guard let data = data else {
dispatchGroup.leave()
completion(.failure(.noData))
return
}
do {
let blogPost = try JSONDecoder().decode(BlogPost.self, from: data)
blogPosts.append(blogPost)
dispatchGroup.leave()
} catch {
dispatchGroup.leave()
completion(.failure(.failedToDecodeJSON))
}
}
dataTask.resume()
}
dispatchGroup.notify(queue: .main) {
completion(.success(blogPosts))
}
}
}

Is there a specific way to append DispatchWorkItems to a DispatchQueue instead of re declaring them in code?

I have several Dispatch work items to execute on a queue i don't want to redeclare the codes, i want to pass them to an array or list of DispatchWorkItems and then inject it to a dispatch queue is there any way to achieve this ?
func executeDispatchWorkItem(url: String, completion : #escaping (Result<String,Error>)-> Void,beganHandler : #escaping (String)-> Void){
do {
beganHandler("\(url) Began to execute ")
let content = try String(contentsOf:URL(string: url)!)
completion(.success(content))
}
catch let error {
completion(.failure(error))
}
sleep(1)
}
var serialQueue = DispatchQueue(label: "A queue")
serialQueue.async {
executeDispatchWorkItem(url: "https://www.google.com/",completion:
{data in
switch data {
case .success(let data):
print("URL : \(data) completed with \(String(describing: data))")
case .failure(let error ):
print("URL : \(error.localizedDescription) failed with \(error.localizedDescription)")
}
}, beganHandler: { me in
print("\(me) began to execute ")
})
executeDispatchWorkItem(url: "www.facebook.com",completion: {data in
switch data {
case .success(let data):
print("URL : \(data) completed with \(String(describing:
data))")
case .failure(let error ):
print("URL : \(error.localizedDescription) failed with \(error.localizedDescription)")
}
}, beganHandler: { me in
print("\(me) began to execute ")
})
executeDispatchWorkItem(url: "www.youtube.com",completion: {data in
switch data {
case .success(let data):
print("URL : \(data) completed with \(String(describing: data))")
case .failure(let error ):
print("URL : \(error.localizedDescription) failed with \(error.localizedDescription)")
}
}, beganHandler: { me in
print("\(me) began to execute ")
})
/// HOW EVER I WANT TO ACHIEVE SOMETHING LIKE THIS
let itemsToExecute : [DispatchWorkItem] = [dispatch1.dispatch2]
// IS THIS POSSIBLE ?
serialQueue.sync(execute: itemsToExecute) ?
Yes, you can have an array of DispatchWorkItem objects, but to dispatch them all, you’d just have to iterate through them, e.g., with either for-in or forEach:
let queue = DispatchQueue(label: "com.domain.app.requests")
let group = DispatchGroup()
let itemsToExecute: [DispatchWorkItem] = [item1, item2]
itemsToExecute.forEach { queue.async(group: group, execute: $0) }
group.notify(queue: .main) {
print("all done") // this is called when the requests are done
}
Note, I used async vs sync, because the whole point of using GCD is to avoid blocking the main queue, and while sync blocks, async doesn’t.
This begs the question of why you’d bother using an array of DispatchWorkItem at all, though. Just add the tasks to the queue directly, and the queue takes care of keeping track of all of them for you.
Frankly, we’d probably just want to use URLSession. For example:
#discardableResult
func request(from urlString: String, completion: #escaping (Result<String,Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: URL(string: urlString)!) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkError.invalidResponse(data, response)))
return
}
guard let string = String(data: data, encoding: .utf8) else {
completion(.failure(NetworkError.nonStringBody))
return
}
completion(.success(string))
}
task.resume()
return task
}
Where perhaps:
enum NetworkError: Error {
case invalidResponse(Data, URLResponse?)
case nonStringBody
}
Then, you can do something like:
for urlString in urlStrings {
group.enter()
request(from: urlString) { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(urlString, error)
case .success(let string):
print(urlString, string.count)
}
}
}
group.notify(queue: .main) {
print("all done")
}

In App Purchases - high percentage of failed purchases - SKError code 2 or 0

We are facing a serious issues with in app purchases in our app.
We offer 3 IAP: auto-renewable subscription 3M, auto-renewable subscription 1Y, non-consumable one-time purchase (LifeTime access)
In our case 70-80% of transactions fail and we mostly get SKError code=0 or code=2 - Cannot connect to iTunes Store. According to SKError documentation it's unknown error (code 0) or cancelled transaction (error 2).
Sometime purchase fails several times for the same user so it’s very hard to believe that user intentionally cancels transaction for the same product 3 or 4 times in a row.
It happens regardless iOS version, device model, our app version.
Below is our code used to fetch products and make a transaction.
We've checked multiple threads with the same issue but coudn't find any solution.
We do not offer any promotions, product identifiers are valid...
Some users are able to make a purchases without any issues.
Any ideas?
import Foundation
import SwiftyStoreKit
import StoreKit
final class IAPService: NSObject {
static let shared = IAPService()
public var isSubscriptionAvailable = false
private var identifiers = ["product_x_id",
"product_y_id",
"product_z_id"]
var products: [SKProduct] = []
var purchaseProducts: [PurchaseProduct] = []
private var successBlock: ((String?,String?)->())? //product Id, receipt
private var errorBlock: ((String)->())?
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: (()->())?
override init() {
super.init()
SKPaymentQueue.default().add(self)
self.loadProducts()
}
func loadProducts(completion: (()->())? = nil) {
productsRequest?.cancel()
productsRequestCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: Set(identifiers))
productsRequest?.delegate = self
productsRequest?.start()
}
func purchaseProduct(identifier: String, onSuccess: ((String?,String?) -> ())?, onError: ((String?) -> ())?) {
guard products.count > 0 else {
loadProducts {
self.purchaseProduct(identifier: identifier, onSuccess: onSuccess, onError: onError)
}
return
}
guard let product = self.products.first(where: { (skProduct) -> Bool in
return skProduct.productIdentifier == identifier
}) else {
onError?("IAP error: cannot find product id")
return
}
clearHandlers()
self.successBlock = onSuccess
self.errorBlock = onError
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(onSuccess: ((String?,String?) -> ())?, onError: ((String?) -> ())?) {
clearHandlers()
self.successBlock = onSuccess
self.errorBlock = onError
SKPaymentQueue.default().restoreCompletedTransactions()
}
public func fetchReceipt(productId: String) {
SwiftyStoreKit.fetchReceipt(forceRefresh: false) { (result) in
switch result {
case .success(let receiptData):
self.successBlock?(productId, receiptData.base64EncodedString(options: []))
self.clearHandlers()
break
case .error(let error):
print("Receipt verification failed: \(error.localizedDescription)")
self.errorBlock?(error.localizedDescription)
self.clearHandlers()
break
}
}
}
private func clearHandlers() {
successBlock = nil
errorBlock = nil
productsRequestCompletionHandler = nil
productsRequest = nil
}
}
extension IAPService: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let skProducts = response.products
skProducts.forEach { (skProduct) in
products.append(skProduct)
}
productsRequestCompletionHandler?()
clearHandlers()
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products: \(error.localizedDescription)")
productsRequestCompletionHandler?()
clearHandlers()
}
}
extension IAPService: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
var failureReason: String = ""
if let skError = transaction.error as? SKError {
switch skError.code { // https://developer.apple.com/reference/storekit/skerror.code
case .unknown:
failureReason = "Unknown or unexpected error occurred"
break
case .paymentCancelled:
failureReason = "Payment cancelled by user"
break
case .clientInvalid:
failureReason = "Invalid Client"
break
case .paymentInvalid:
failureReason = "Invalid Payment"
break
case .paymentNotAllowed:
failureReason = "Payment not allowed"
break
case .cloudServiceNetworkConnectionFailed:
failureReason = "Cloud service network connection failed"
break
case .cloudServicePermissionDenied:
failureReason = "Cloud service permission denied"
break
case .storeProductNotAvailable:
failureReason = "Store product not available"
break
case .cloudServiceRevoked:
failureReason = "Cloud service revoked"
break
case .privacyAcknowledgementRequired:
failureReason = "Privacy Acknowledgement Required"
break
case .unauthorizedRequestData:
failureReason = "Unauthorized Request Data"
break
case .invalidOfferIdentifier:
failureReason = "Invalid offer identifier"
break
case .invalidSignature:
failureReason = "Invalid signature"
break
case .missingOfferParams:
failureReason = "Missing offer params"
break
case .invalidOfferPrice:
failureReason = "Invalid offer price"
break
}
failureReason += " code: \(skError.code.rawValue)"
}
else if let isCancelledError = transaction.error?.isCancelledError, isCancelledError == true {
failureReason = "isCancelledError"
}
else {
failureReason = "\(transaction.error.debugDescription)"
}
SKPaymentQueue.default().finishTransaction(transaction)
errorBlock?(failureReason)
self.clearHandlers()
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
fetchReceipt(productId: identifier)
}
}
I have had the same issue where I get either error code 0 or error code 2 (usually the error code 2 is triggered even when the purchase was successful) and been e-mailing back and forth with Apple's Developer Technical Support for the past few months on this one.
It looks to be a known issue which happens for certain apps with Apple currently, the best thing to do in your case would be to raise a Technical Support Incident (TSI) for code-level support from Apple directly.
Alternatively you can also use the feedback assistant to raise a bug report.
Unfortunately this seems to be something caused on the Store Kit/App Store side and requires Apple to look into it on a case-by-case basis where it does occur.