Как выполнить синхронный вызов API после асинхронного вызова API - PullRequest
0 голосов
/ 03 ноября 2019

У меня есть две службы, которые работают совершенно независимо, одна - синхронный вызов для получения списков покупок, а другая - асинхронный вызов для добавления списков покупок. Проблема возникает, когда я пытаюсь получить списки покупок сразу после успешного завершения вызова 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()
    }

Получить shoppingList

    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 ]

1 голос
/ 03 ноября 2019

Этот вопрос основан на ошибочном предположении, что вам нужен этот синхронный запрос.

Вы предположили, что вам это нужно для тестирования. Это не так: каждый использует «ожидания» для тестирования асинхронных процессов;мы не оптимизируем код для целей тестирования.

Вы также предложили «остановить все процессы», пока запрос не будет выполнен. Опять же, это не так и предлагает ужасный UX и может привести к тому, что ваше приложение будет убито сторожевым процессом, если вы сделаете это в неподходящее время, находясь в медленной сети. Если на самом деле пользовательский интерфейс должен быть заблокирован во время выполнения запроса, мы обычно просто выбрасываем 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, это спорно, что вы должны быть модульным тестированием запроса сервера / ответа на все. Часто вместо этого макет сетевой службы , или используйте URLProtocol до макет его за кадром .

Для получения дополнительной информации об асинхронных тестах см. Асинхронные тестыи Ожидания .


К вашему сведению, вышеприведенное использует рефакторинг 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
}
0 голосов
/ 03 ноября 2019

Я уверен, что это могло бы быть намного лучше, но это моя 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)

            })

        }
    }

}

...