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

Getting nil data when try to cancel a task and restart it again 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 秒用一个定时器调用这个请求。但是每当我第二次尝试调用它时,我都会得到 nildata 值。

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}

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

从错误来看,似乎是 race condition(我可能是错的)类型的情况,在您初始化新的 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 可以再次重新启动。

我知道这并不能准确找到您代码中的错误,但看看这是否有助于您的用例并简化事情?