Этот вопрос основан на ошибочном предположении, что вам нужен этот синхронный запрос.
Вы предположили, что вам это нужно для тестирования. Это не так: каждый использует «ожидания» для тестирования асинхронных процессов;мы не оптимизируем код для целей тестирования.
Вы также предложили «остановить все процессы», пока запрос не будет выполнен. Опять же, это не так и предлагает ужасный 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
}