尝试取消任务并再次重新启动 Swift 时获取零数据

发布于 2025-01-10 12:28:58 字数 7829 浏览 3 评论 0原文

我想做的事情:
有时我的 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 秒调用一次此请求。但每当我尝试第二次调用它时,我都会得到 datanil 值。

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

戈亓 2025-01-17 12:28:58

似乎您重新初始化 URLSessionTask 的方式对我来说看起来不错,但我认为取消任务并使计时器无效时似乎会发生两次,正如您在 code 2code 4 所以调试起来有点棘手。

从错误来看,它似乎是一种竞争条件(我可能是错的)类型的情况,当您初始化新的 URLSession 时,url 会话会被取消。

如果您想尝试,我可以提供一个更简单的替代方案:

在创建 URLRequest 时,不要使用计时器,而是使用 timeoutInterval

storeRequest.httpMethod = "POST" 
storeRequest.httpBody = requestData

// add this
storeRequest.timeoutInterval = timeOutInterval

然后在您的数据任务中,处理程序您可以检查是否由于超时而遇到错误:

let task = session.dataTask(with: storeRequest) { [weak self] (data, response, error) in
    
    do {
        
        if let error = error
        {
            // This checks if the request timed out based on your interval
            if (error as? URLError)?.code == .timedOut
            {
                // retry your request
                self?.receiptValidation(completion: completion)
            }
        }

在我看来,重试的完整机制似乎变得更加简单,您不需要管理 URLSessionTask 对象

这是完整的代码:

private func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool,
                                                     _ error: Error?) -> ())
{
    // all your initial work to prepare your data and jsonDict data
    // goes here
    
    do {
        let requestData = try JSONSerialization.data(withJSONObject: jsonDict!,
                                                     options: .prettyPrinted)
        
        let storeURL = URL(string: self.verifyReceiptURL)!
        var storeRequest = URLRequest(url: storeURL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = requestData
        storeRequest.timeoutInterval = timeOutInterval
        
        let session = URLSession(configuration: .default)
        
        let task = session.dataTask(with: storeRequest)
        { [weak self] (data, response, error) in
            
            do {
                
                if let error = error
                {
                    // Error due to a timeout
                    if (error as? URLError)?.code == .timedOut
                    {
                        // retry your request
                        self?.receiptValidation(completion: completion)
                    }
                    
                    // some other error
                }
                
                if let data = data,
                   let jsonResponse
                    = try JSONSerialization.jsonObject(with: data,
                                                       options: .mutableContainers) as? NSDictionary
                {
                    // do your work or call completion handler
                    print("JSON: \(jsonResponse)")
                }
            }
            catch
            {
                print("Response error: \(error)")
            }
        }
        
        task.resume()
    }
    catch
    {
        print("Request creation error: \(error)")
    }
}

我正在重试请求当我弄清楚请求后立即超时,但是您会显示警报,而不是要求用户重试。

一旦他们点击重试,您所需要做的就是再次调用您的函数 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 in code 2 and code 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 the timeoutInterval

storeRequest.httpMethod = "POST" 
storeRequest.httpBody = requestData

// add this
storeRequest.timeoutInterval = timeOutInterval

Then inside your data task, handler you could check if you encounter the error due to a timeout:

let task = session.dataTask(with: storeRequest) { [weak self] (data, response, error) in
    
    do {
        
        if let error = error
        {
            // This checks if the request timed out based on your interval
            if (error as? URLError)?.code == .timedOut
            {
                // retry your request
                self?.receiptValidation(completion: completion)
            }
        }

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:

private func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool,
                                                     _ error: Error?) -> ())
{
    // all your initial work to prepare your data and jsonDict data
    // goes here
    
    do {
        let requestData = try JSONSerialization.data(withJSONObject: jsonDict!,
                                                     options: .prettyPrinted)
        
        let storeURL = URL(string: self.verifyReceiptURL)!
        var storeRequest = URLRequest(url: storeURL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = requestData
        storeRequest.timeoutInterval = timeOutInterval
        
        let session = URLSession(configuration: .default)
        
        let task = session.dataTask(with: storeRequest)
        { [weak self] (data, response, error) in
            
            do {
                
                if let error = error
                {
                    // Error due to a timeout
                    if (error as? URLError)?.code == .timedOut
                    {
                        // retry your request
                        self?.receiptValidation(completion: completion)
                    }
                    
                    // some other error
                }
                
                if let data = data,
                   let jsonResponse
                    = try JSONSerialization.jsonObject(with: data,
                                                       options: .mutableContainers) as? NSDictionary
                {
                    // do your work or call completion handler
                    print("JSON: \(jsonResponse)")
                }
            }
            catch
            {
                print("Response error: \(error)")
            }
        }
        
        task.resume()
    }
    catch
    {
        print("Request creation error: \(error)")
    }
}

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 that receiptValidation 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 ?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文