尝试取消任务并再次重新启动 Swift 时获取零数据
我想做的事情:
有时我的 URLSession
调用请求需要很长时间才能得到响应。这就是为什么如果它在 60 秒内没有给出响应,我会尝试再次调用该呼叫请求。 60 秒后,它将取消呼叫请求,然后提醒用户重试。当用户点击重试警报按钮时,它将从头开始再次调用呼叫请求。
我如何尝试:
我为会话任务声明一个全局变量,如下所示:
private weak var IAPTask: URLSessionTask?
这是我的调用请求函数:
代码 1
func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool, _ error: Error?) -> ()) {
let receiptFileURL = Bundle.main.appStoreReceiptURL
guard let receiptData = try? Data(contentsOf: receiptFileURL!) else {
//This is the First launch app VC pointer call
completion(false, nil)
return
}
let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString as AnyObject, "password" : AppSpecificSharedSecret as AnyObject]
do {
let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
let storeURL = URL(string: self.verifyReceiptURL)!
var storeRequest = URLRequest(url: storeURL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
if let latestInfoReceiptObjects = self?.getLatestInfoReceiptObjects(jsonResponse: jsonResponse) {
self?.getCurrentTimeFromServer(completionHandler: { currentDateFromServer in
let purchaseStatus = self?.isPurchaseActive(currentDateFromServer: currentDateFromServer, latestReceiptInfoArray: latestInfoReceiptObjects)
completion(purchaseStatus!, nil)
})
}
}
} catch let parseError {
completion(false, parseError)
}
})
task.resume()
self.IAPTask = task
} catch let parseError {
completion(false, parseError)
}
}
我使用计时器每 60 秒调用一次此请求。但每当我尝试第二次调用它时,我都会得到 data
的 nil
值。
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
//Getting data = nil here for second time
})
让我展示如何逐步取消全局变量。
首先在我设置计时器并调用呼叫请求的地方调用它。如果我在 10 秒内收到响应(出于测试目的,我将其设置为 10),我将取消计时器和呼叫请求任务并执行进一步的程序。 :
代码2
func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
let infoDic: [String : String] = ["IAPReceiptValidationFrom" : iapReceiptValidationFrom.rawValue]
self.IAPTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.IAPTimerAction), userInfo: infoDic, repeats: false)
IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in
if isSuccessful == true {
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
viewController.dismiss(animated: true, completion: nil)
self.hideActivityIndicator()
}
}
}
}
}
这是我调用调用请求的完成。如果它只是给我响应,我会为完成设置一个真实值。
代码 3
func IAPStatusCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom, complition: @escaping (_ isSuccessful: Bool)->()) {
receiptValidation() { isPurchaseSchemeActive, error in
if let err = error {
self.onBuyProductHandler?(.failure(err))
} else {
self.onBuyProductHandler?(.success(isPurchaseSchemeActive))
}
complition(true)
}
}
这是计时器操作,我从这里使计时器无效并取消任务调用请求,然后向用户显示弹出警报:
代码 4
@objc func IAPTimerAction(sender: Timer) {
if let dic = (sender.userInfo)! as? Dictionary<String, String> {
let val = dic["IAPReceiptValidationFrom"]!
let possibleType = IAPReceiptValidationFrom(rawValue: val)
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.showAlertForRetryIAP(iapReceiptValidationFrom: possibleType!)
}
}
最后,在警报“重试”操作中调用相同的初始响应检查函数。
代码 5
func showAlertForRetryIAP(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
DispatchQueue.main.async {
let alertVC = UIAlertController(title: "Time Out!" , message: "Apple server seems busy. Please wait or try again.", preferredStyle: UIAlertController.Style.alert)
alertVC.view.tintColor = UIColor.black
let okAction = UIAlertAction(title: "Try again", style: UIAlertAction.Style.cancel) { (alert) in
self.showActivityIndicator()
self.IAPResponseCheck(iapReceiptValidationFrom: iapReceiptValidationFrom)
}
alertVC.addAction(okAction)
DispatchQueue.main.async {
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
var presentVC = viewController
while let next = presentVC.presentedViewController {
presentVC = next
}
presentVC.present(alertVC, animated: true, completion: nil)
}
}
}
}
}
这是我得到的响应:
▿ Optional<NSURLResponse>
- some : <NSHTTPURLResponse: 0x281d07680> { URL: https://sandbox.itunes.apple.com/verifyReceipt } { Status Code: 200, Headers {
Connection = (
"keep-alive"
);
"Content-Type" = (
"application/json"
);
Date = (
"Sun, 27 Feb 2022 17:28:17 GMT"
);
Server = (
"daiquiri/3.0.0"
);
"Strict-Transport-Security" = (
"max-age=31536000; includeSubDomains"
);
"Transfer-Encoding" = (
Identity
);
"apple-originating-system" = (
CommerceGateway
);
"apple-seq" = (
"0.0"
);
"apple-timing-app" = (
"154 ms"
);
"apple-tk" = (
false
);
b3 = (
"48d6c45fe76eb9d8445d83e518f01866-c8bfb32d2a7305e2"
);
"x-apple-jingle-correlation-key" = (
JDLMIX7HN245QRC5QPSRR4AYMY
);
"x-apple-request-uuid" = (
"48d6c45f-e76e-b9d8-445d-83e518f01866"
);
"x-b3-spanid" = (
c8bfb32d2a7305e2
);
"x-b3-traceid" = (
48d6c45fe76eb9d8445d83e518f01866
);
"x-daiquiri-instance" = (
"daiquiri:45824002:st44p00it-hyhk15104701:7987:22RELEASE11:daiquiri-amp-commerce-clients-ext-001-st"
);
"x-responding-instance" = (
"CommerceGateway:020115:::"
);
} }
这是错误:
▿ Optional<Error>
- some : Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://sandbox.itunes.apple.com/verifyReceipt, NSErrorFailingURLKey=https://sandbox.itunes.apple.com/verifyReceipt, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>, NSLocalizedDescription=cancelled}
What I am trying to do:
Sometimes my URLSession
call request takes a too long time to give a response back. That's why I am trying to call the call request again if it doesn't give a response back within 60 seconds. After 60 seconds it will cancel the call request, then give an alert to the user to try again. When the user taps on the try again alert button it will call the call request again from the beginning.
How I tried:
I declare a global variable for the session task like this:
private weak var IAPTask: URLSessionTask?
This is my call request function:
Code 1
func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool, _ error: Error?) -> ()) {
let receiptFileURL = Bundle.main.appStoreReceiptURL
guard let receiptData = try? Data(contentsOf: receiptFileURL!) else {
//This is the First launch app VC pointer call
completion(false, nil)
return
}
let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString as AnyObject, "password" : AppSpecificSharedSecret as AnyObject]
do {
let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
let storeURL = URL(string: self.verifyReceiptURL)!
var storeRequest = URLRequest(url: storeURL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
if let latestInfoReceiptObjects = self?.getLatestInfoReceiptObjects(jsonResponse: jsonResponse) {
self?.getCurrentTimeFromServer(completionHandler: { currentDateFromServer in
let purchaseStatus = self?.isPurchaseActive(currentDateFromServer: currentDateFromServer, latestReceiptInfoArray: latestInfoReceiptObjects)
completion(purchaseStatus!, nil)
})
}
}
} catch let parseError {
completion(false, parseError)
}
})
task.resume()
self.IAPTask = task
} catch let parseError {
completion(false, parseError)
}
}
I am calling this request after every 60 seconds with a timer. But whenever I try to call it for a second time, I am getting nil
value for data
.
let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
//Getting data = nil here for second time
})
Let me show how I am canceling the global variable step by step.
First calling this where I am setting the timer and call for the call request. If I get the response within 10 seconds (For testing purposes I set it as 10), I am canceling the timer and the call request task and doing further procedures. :
Code 2
func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
let infoDic: [String : String] = ["IAPReceiptValidationFrom" : iapReceiptValidationFrom.rawValue]
self.IAPTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.IAPTimerAction), userInfo: infoDic, repeats: false)
IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in
if isSuccessful == true {
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
viewController.dismiss(animated: true, completion: nil)
self.hideActivityIndicator()
}
}
}
}
}
This is the completion where I call the call request. I set a true value for the completion if it just gives me the response.
Code 3
func IAPStatusCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom, complition: @escaping (_ isSuccessful: Bool)->()) {
receiptValidation() { isPurchaseSchemeActive, error in
if let err = error {
self.onBuyProductHandler?(.failure(err))
} else {
self.onBuyProductHandler?(.success(isPurchaseSchemeActive))
}
complition(true)
}
}
This is the timer action from where I am invalidating the timer and canceling the task call request and then showing the pop up alert to the user:
Code 4
@objc func IAPTimerAction(sender: Timer) {
if let dic = (sender.userInfo)! as? Dictionary<String, String> {
let val = dic["IAPReceiptValidationFrom"]!
let possibleType = IAPReceiptValidationFrom(rawValue: val)
self.IAPTimer.invalidate()
self.IAPTask?.cancel()
self.showAlertForRetryIAP(iapReceiptValidationFrom: possibleType!)
}
}
And finally, call the same initial response check function in the alert "Try again" action.
Code 5
func showAlertForRetryIAP(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
DispatchQueue.main.async {
let alertVC = UIAlertController(title: "Time Out!" , message: "Apple server seems busy. Please wait or try again.", preferredStyle: UIAlertController.Style.alert)
alertVC.view.tintColor = UIColor.black
let okAction = UIAlertAction(title: "Try again", style: UIAlertAction.Style.cancel) { (alert) in
self.showActivityIndicator()
self.IAPResponseCheck(iapReceiptValidationFrom: iapReceiptValidationFrom)
}
alertVC.addAction(okAction)
DispatchQueue.main.async {
self.getTopVisibleViewController { topViewController in
if let viewController = topViewController {
var presentVC = viewController
while let next = presentVC.presentedViewController {
presentVC = next
}
presentVC.present(alertVC, animated: true, completion: nil)
}
}
}
}
}
This is the response I am getting:
▿ Optional<NSURLResponse>
- some : <NSHTTPURLResponse: 0x281d07680> { URL: https://sandbox.itunes.apple.com/verifyReceipt } { Status Code: 200, Headers {
Connection = (
"keep-alive"
);
"Content-Type" = (
"application/json"
);
Date = (
"Sun, 27 Feb 2022 17:28:17 GMT"
);
Server = (
"daiquiri/3.0.0"
);
"Strict-Transport-Security" = (
"max-age=31536000; includeSubDomains"
);
"Transfer-Encoding" = (
Identity
);
"apple-originating-system" = (
CommerceGateway
);
"apple-seq" = (
"0.0"
);
"apple-timing-app" = (
"154 ms"
);
"apple-tk" = (
false
);
b3 = (
"48d6c45fe76eb9d8445d83e518f01866-c8bfb32d2a7305e2"
);
"x-apple-jingle-correlation-key" = (
JDLMIX7HN245QRC5QPSRR4AYMY
);
"x-apple-request-uuid" = (
"48d6c45f-e76e-b9d8-445d-83e518f01866"
);
"x-b3-spanid" = (
c8bfb32d2a7305e2
);
"x-b3-traceid" = (
48d6c45fe76eb9d8445d83e518f01866
);
"x-daiquiri-instance" = (
"daiquiri:45824002:st44p00it-hyhk15104701:7987:22RELEASE11:daiquiri-amp-commerce-clients-ext-001-st"
);
"x-responding-instance" = (
"CommerceGateway:020115:::"
);
} }
And this is the error:
▿ Optional<Error>
- some : Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://sandbox.itunes.apple.com/verifyReceipt, NSErrorFailingURLKey=https://sandbox.itunes.apple.com/verifyReceipt, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>, NSLocalizedDescription=cancelled}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
似乎您重新初始化
URLSessionTask
的方式对我来说看起来不错,但我认为取消任务并使计时器无效时似乎会发生两次,正如您在code 2
和code 4
所以调试起来有点棘手。从错误来看,它似乎是一种竞争条件(我可能是错的)类型的情况,当您初始化新的 URLSession 时,url 会话会被取消。
如果您想尝试,我可以提供一个更简单的替代方案:
在创建
URLRequest
时,不要使用计时器,而是使用timeoutInterval
然后在您的数据任务中,处理程序您可以检查是否由于超时而遇到错误:
在我看来,重试的完整机制似乎变得更加简单,您不需要管理 URLSessionTask 对象
这是完整的代码:
我正在重试请求当我弄清楚请求后立即超时,但是您会显示警报,而不是要求用户重试。
一旦他们点击重试,您所需要做的就是再次调用您的函数
receiptValidation(completion:completion)
因此,完成闭包可能就是需要存储的内容,以便receiptValidation
可以再次重新启动。我知道这并不能准确地找到您代码中的错误,但看看这是否可以帮助您的用例并简化事情?
It seems that the way you reinitialize the
URLSessionTask
looks fine to me but I think there is something when cancelling a task and invalidating the timer seems to happen twice as you mentioned incode 2
andcode 4
so it is a little tricky to debug.From the error it seems like a
race condition
(I could be wrong) type situation where while you are initializing a new URLSession the url session gets cancelled.What I can offer is a simpler alternative if you would like to try:
When creating your
URLRequest
, instead of using a timer, use thetimeoutInterval
Then inside your data task, handler you could check if you encounter the error due to a timeout:
The full mechanism for retrying seems to become a lot more simpler in my opinion and you don't need to manage a URLSessionTask object
This is the full code:
I am retrying the request immediately when I figure out the request timed out, however you would show your alert instead to ask the user to retry.
Once they hit retry, all you need to do is call your function again
receiptValidation(completion: completion)
so probably the completion closure is what needs to be stored so thatreceiptValidation
can be relaunched again.I know this does not exactly find the error in your code but have a look if this could help with your use case and simplify things ?