Swift 合并递归重试

Swift combine recursive retry

我想在服务器响应特定消息(在示例中为 401 错误)时使用 Swift Combine 执行递归一次重试。该响应中的数据改变了模型,允许单次重试。

我为 iOS 13

之前使用的结果类型写了一个小扩展
extension URLSession {
    typealias HTTPResponse = (response: HTTPURLResponse, data: Data)
    typealias DataTaskResult = ((Result<HTTPResponse, Error>) -> Void)

    func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTask {
        self.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completionHandler(.failure(error))
            }
            completionHandler(.success((response as! HTTPURLResponse, data!)))
        }
    }
}

我使用此扩展程序执行以下操作

class Account {
    enum CommunicationError: Swift.Error {
        case counterOutOfSync
    }

    var counter: Int = 0

    func send(isRetry: Bool = false, completionBlock: @escaping URLSession.DataTaskResult) {
        var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
        request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
        request.httpMethod = "POST"

        URLSession.shared.dataTask(with: request) { [weak self] taskResult in
            do {
                let taskResponse = try taskResult.get()
                if taskResponse.response.statusCode == 401 {
                    if isRetry { throw CommunicationError.counterOutOfSync }

                    // Counter is resynced based on taskResponse.data
                    self?.send(isRetry: true, completionBlock: completionBlock)
                } else {
                    completionBlock(.success(taskResponse))
                }
            } catch {
                completionBlock(.failure(error))
            }
        }.resume()
    }
}

可以看到函数中的递归调用。我想对 Combine 做同样的事情,但我不知道该怎么做。据我所知

func combine(isRetry: Bool = false) -> AnyPublisher<Data, Error> {
    var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
    request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
    request.httpMethod = "POST"

    return URLSession.shared.dataTaskPublisher(for: request).tryMap {
        let response = [=13=].response as! HTTPURLResponse
        if response.statusCode == 401 {
            if isRetry { throw CommunicationError.counterOutOfSync }

            // Counter is resynced based on [=13=].data
            return self.combine(isRetry: true)
        } else {
            return [=13=].data
        }
    }.eraseToAnyPublisher()
}

感谢任何帮助

如果您有原版 send(isRetry:completionBlock:),您可以使用 Future 将其转换为发布者:

func send() -> AnyPublisher<URLSession.HTTPResponse, Error> {
    Future { [weak self] promise in
        self?.send(isRetry: false) { result in promise(result) }
    }
    .eraseToAnyPublisher()
}


或者,Combine 已经有一个 .retry 运算符,所以整个事情可以纯粹在 Combine 中完成:

URLSession.shared.dataTaskPublisher(for: request)
    .tryMap { data, response in
        let response = response as! HTTPURLResponse
        if response.statusCode == 401 {
            throw CommunicationError.counterOutOfSync
        } else {
            return (response: response, data: data)
        }
    }
    .retry(1)
    .eraseToAnyPublisher()

每当上游出现 any 错误(不仅仅是 401)时,这将重试一次。您可以尝试更多以仅在某些条件下重试(例如,参见