异步api调用后如何执行同步api调用

时间:2019-11-03 10:37:41

标签: swift asynchronous synchronous urlsession

我有两个完全独立运行的服务,一个是获取购物清单的同步调用,另一个是添加购物清单的异步调用。问题是在add-Shopping-lists呼叫成功完成后,我尝试获取购物清单。

获取购物清单的功能永不返回它,只是在我在add-Shopping-lists功能关闭时将其挂起后才挂起。在没有承诺的情况下拨打这两个电话的最佳方法是什么。

创建ShoppingList

    func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: String) -> URLRequest {

        guard let accessToken = UserSessionInfo.accessToken else {
            fatalError("Nil access token")
        }

        let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)

        guard let requestUrl = URLComponents(string: urlString!)?.url else {
            fatalError("Nil url")
        }

        var request = URLRequest(url:requestUrl)
        request.httpMethod = method
        request.httpBody = try! data?.jsonString()?.data(using: .utf8)
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

        return request
    }

    func createShoppingList(with shoppingList: ShoppingList, completion: @escaping (Bool, Error?) -> Void) {

        let serviceURL = environment + Endpoint.createList.rawValue
        let request = createURLRequest(with: serviceURL, data: shoppingList, httpMethod: HttpBody.post.rawValue)
        let session = URLSession.shared

        let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in

            guard let _ = data,
                let response = response as? HTTPURLResponse,
                (200 ..< 300) ~= response.statusCode,
                error == nil else {
                    completion(false, error)
                    return
            }

            completion(true, nil)
        })

        task.resume()
    }

获取购物清单

    func fetchShoppingLists(with customerId: String) throws -> [ShoppingList]? {

        var serviceResponse: [ShoppingList]?
        var serviceError: Error?

        let serviceURL = environment + Endpoint.getLists.rawValue + customerId
        let request = createURLRequest(with: serviceURL, httpMethod: HttpBody.get.rawValue)
        let semaphore = DispatchSemaphore(value: 0)
        let session = URLSession.shared

        let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in

            defer { semaphore.signal() }

            guard let data = data,                            // is there data
                let response = response as? HTTPURLResponse,  // is there HTTP response
                (200 ..< 300) ~= response.statusCode,         // is statusCode 2XX
                error == nil else {                           // was there no error, otherwise ...
                     serviceError = error
                    return
            }

            do {
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                let shoppingList = try decoder.decode([ShoppingList].self, from: data)
                 serviceResponse = shoppingList
            } catch let error {
                 serviceError = error
            }

            })

        task.resume()

        semaphore.wait()

        if let error = serviceError {
            throw error
        }

        return serviceResponse

    }

功能的使用

    func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: @escaping (Bool, Error?) -> Void) {

        shoppingListService.createShoppingList(with: shoppingList, completion: { (success, error) in
            if success {

                self.shoppingListCache.clearCache()

                let serviceResponse =  try? self.fetchShoppingLists(with: customerId)

                if let _ = serviceResponse {
                    completion(true, nil)
                } else {
                    let fetchListError =  NSError().error(description: "Unable to fetch shoppingLists")
                    completion(false, fetchListError)
                }

            } else {
                completion(false, error)
            }
        })

    }

我想调用 fetchShoppingLists (这是一个同步调用)并获取新数据,然后成功调用完成块。

2 个答案:

答案 0 :(得分:1)

该问题基于一个错误的假设,即您需要此同步请求。

您建议您需要此进行测试。事实并非如此:有人使用“期望”来测试异步过程;我们不会针对测试进行代码优化。

您还建议您要“停止所有进程”,直到请求完成为止。同样,这是不正确的,它提供了可怕的用户体验,并且如果您在慢速网络上在错误的时间执行此操作,可能会使您的应用程序被监视程序杀死。如果实际上在请求进行过程中需要阻止UI,那么我们通常只是在整个UI的变暗/模糊视图之上抛出UIActivityIndicatorView(又称“旋转器”)防止用户与可见控件进行交互(如果有)。

但是,最重要的是,我知道同步请求感觉如此直观且合乎逻辑,但这始终是错误的方法。

无论如何,我会让fetchShoppingLists异步:

func fetchShoppingLists(with customerId: String, completion: @escaping (Result<[ShoppingList], Error>) -> Void) {
    var serviceResponse: [ShoppingList]?

    let serviceURL = environment + Endpoint.getLists.rawValue + customerId
    let request = createURLRequest(with: serviceURL, httpMethod: .get)
    let session = URLSession.shared

    let task = session.dataTask(with: request) { data, response, error in
        guard let data = data,                            // is there data
            let response = response as? HTTPURLResponse,  // is there HTTP response
            200 ..< 300 ~= response.statusCode,         // is statusCode 2XX
            error == nil else {                           // was there no error, otherwise ...
                completion(.failure(error ?? ShoppingError.unknownError))
                return
        }

        do {
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let shoppingList = try decoder.decode([ShoppingList].self, from: data)
            completion(.success(shoppingList))
        } catch let jsonError {
            completion(.failure(jsonError))
        }
    }

    task.resume()
}

然后您只需采用这种异步模式。请注意,虽然我将Result模式用于完成处理程序,但我还是留给您使用,因为这是为了最大程度地减少集成问题:

func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: @escaping (Bool, Error?) -> Void) {
    shoppingListService.createShoppingList(with: shoppingList) { success, error in
        if success {
            self.shoppingListCache.clearCache()

            self.fetchShoppingLists(with: customerId) { result in
                switch result {
                case .failure(let error):
                    completion(false, error)

                case .success:
                    completion(true, nil)
                }
            }
        } else {
            completion(false, error)
        }
    }
}

例如,现在,您建议您使fetchShoppingLists同步以便于测试。您可以使用“期望”轻松测试异步方法:

class MyAppTests: XCTestCase {

    func testFetch() {
        let exp = expectation(description: "Fetching ShoppingLists")

        let customerId = ...

        fetchShoppingLists(with: customerId) { result in
            if case .failure(_) = result {
                XCTFail("Fetch failed")
            }
            exp.fulfill()
        }

        waitForExpectations(timeout: 10)
    }
}

FWIW,您完全应该对服务器的请求/响应进行单元测试是有争议的。取而代之的是mock the network service,或使用URLProtocolmock it behind the scenes

有关异步测试的更多信息,请参见Asynchronous Tests and Expectations


仅供参考,以上代码使用了重构的createURLRequest,它使用了最后一个参数而不是String的枚举。枚举的整个思想是使不可能传递无效参数成为可能,因此让我们在此处而不是在调用点进行rawValue转换:

enum HttpMethod: String {
    case post = "POST"
    case get = "GET"
}

func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: HttpMethod) -> URLRequest {
    guard let accessToken = UserSessionInfo.accessToken else {
        fatalError("Nil access token")
    }

    guard
        let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
        let requestUrl = URLComponents(string: urlString)?.url 
    else {
        fatalError("Nil url")
    }

    var request = URLRequest(url: requestUrl)
    request.httpMethod = method.rawValue
    request.httpBody = try! data?.jsonString()?.data(using: .utf8)
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

    return request
}

答案 1 :(得分:0)

我敢肯定它会好很多,但这是我的5分钟版本。


import Foundation
import UIKit

struct Todo: Codable {
    let userId: Int
    let id: Int
    let title: String
    let completed: Bool
}

enum TodoError: String, Error {
    case networkError
    case invalidUrl
    case noData
    case other
    case serializationError
}

class TodoRequest {

    let todoUrl = URL(string: "https://jsonplaceholder.typicode.com/todos")

    var todos: [Todo] = []

    var responseError: TodoError?

    func loadTodos() {

        var responseData: Data?

        guard let url = todoUrl else { return }
        let group = DispatchGroup()

        let task = URLSession.shared.dataTask(with: url) { [weak self](data, response, error) in
                responseData = data
                self?.responseError = error != nil ? .noData : nil
                group.leave()
        }

        group.enter()
        task.resume()
        group.wait()

        guard responseError == nil else { return }

        guard let data = responseData else { return }

        do {
            todos = try JSONDecoder().decode([Todo].self, from: data)
        } catch {
            responseError = .serializationError
        }

    }

    func retrieveTodo(with id: Int, completion: @escaping (_ todo: Todo? , _ error: TodoError?) -> Void) {
        guard var url = todoUrl else { return }

        url.appendPathComponent("\(id)")

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let todoData = data else { return completion(nil, .noData) }
            do {
                let todo = try JSONDecoder().decode(Todo.self, from: todoData)
                completion(todo, nil)
            } catch {
                completion(nil, .serializationError)
            }
        }

        task.resume()
    }
}

class TodoViewController: UIViewController {

    let request = TodoRequest()

    override func viewDidLoad() {
        super.viewDidLoad()

        DispatchQueue.global(qos: .background).async { [weak self] in

            self?.request.loadTodos()

            self?.request.retrieveTodo(with: 1, completion: { [weak self](todoData, error) in
                guard let strongSelf = self else { return }

                if let todoError = error {
                    return debugPrint(todoError.localizedDescription)
                }

                guard let todo = todoData else {
                    return debugPrint("No todo")
                }

                debugPrint(strongSelf.request.todos)
                debugPrint(todo)

            })

        }
    }

}