Разработка
Как использовать URLSession с Async/Await для сетевых запросов в Swift
Современные API Swift в сочетании с URLSession и async/await позволяют написать надежный сетевой слой, не нуждаясь в сторонних зависимостях.
URLSession позволяет выполнять сетевые запросы и становится еще более мощным благодаря API async/await. Вы можете запросить данные с целевого URL и спарсить их в декодированную структуру перед отображением в представлении.
Популярные фреймворки, такие как Alamofire, стремятся упростить выполнение запросов, но для многих приложений можно обойтись и без сторонних решений. В этой статье мы расскажем об основах выполнения API-запросов и декодирования JSON-данных с помощью Swift Concurrency.
Выполнение сетевого запроса с помощью async/await
Вы можете использовать URLSession для выполнения запросов по заданному URL следующим образом:
/// Configure the URL for our request. /// In this case, an example JSON response from httpbin. let url = URL(string: "https://httpbin.org/get")! /// Use URLSession to fetch the data asynchronously. let (data, response) = try await URLSession.shared.data(from: url)
Мы получаем данные для обычного URL-адреса, и в случае успешного выполнения запроса мы получаем обратно данные и объект ответа. Этот метод также будет успешным, если запрос вернет недействительный код состояния. Например, запрос 404 not found все равно приведет к получению данных и объекта ответа без выброса ошибки.
Передача аргументов в GET-запросе
В примере выше представлен простой GET-запрос без параметров. Мы можем добавить параметры с помощью URLComponents:
var urlComponents = URLComponents(string: "https://httpbin.org/get")! /// Define the parameters. let parameters: [String: String] = [ "name": "Antoine van der Lee", "age": "33" ] /// Add the query parameters to the URL. urlComponents.queryItems = parameters.map { key, value in URLQueryItem(name: key, value: value) } /// Ensure we have a valid URL and throw a URLError if it fails. guard let url = urlComponents.url else { throw URLError(.badURL) } /// Use URLSession to fetch the data asynchronously. let (data, response) = try await URLSession.shared.data(from: url)
В приведенном выше примере получается следующий URL с закодированными параметрами:
https://httpbin.org/get?age=33&name=Antoine%20van%20der%20Lee
Выполнение POST-запроса с параметрами
POST-запрос работает по-другому и требует настройки метода HTTP. Мы должны закодировать параметры как JSONи настроить заголовок content-type. В целом код выглядит следующим образом:
/// Configure the URL for our request. let url = URL(string: "https://httpbin.org/post")! /// Create a URLRequest for the POST request. var request = URLRequest(url: url) /// Configure the HTTP method. request.httpMethod = "POST" /// Configure the proper content-type value to JSON. request.setValue("application/json", forHTTPHeaderField: "Content-Type") /// Define the struct of data and encode it to data. let postData = PostData(name: "Antoine van der Lee", age: 33) let jsonData = try JSONEncoder().encode(postData) /// Pass in the data as the HTTP body. request.httpBody = jsonData /// Use URLSession to fetch the data asynchronously. let (data, response) = try await URLSession.shared.data(for: request)
Я рекомендую использовать структуру для определения параметров, чтобы сделать ваш код менее подверженным ошибкам. Для приведенного выше примера структура выглядит следующим образом:
/// Define a struct to represent the data you want to send struct PostData: Codable { let name: String let age: Int }
Декодирование ответов JSON в Decodable структуру
Теперь, когда мы знаем, как отправлять данные, пришло время заняться декодированием JSON-ответов. Я всегда проверяю сетевой трафик с помощью симулятора Xcode, так что в процессе разработки я могу быстро просмотреть возвращаемый JSON:
Замечательно то, что RocketSim всегда будет работать в фоновом режиме, поэтому вы также можете использовать его для проверки сетевых запросов, которые неожиданно завершились неудачей. Представьте, сколько времени вы сэкономите, если не будете искать, как воспроизвести сбой запроса!
Мы можем декодировать данный JSON-ответ с помощью JSON-декодера. Чтобы это сработало, нам сначала нужно определить JSON-ответ как декодируемую структуру:
/// Define a struct to handle the response from httpbin.org. struct PostResponse: Decodable { /// In this case, we can reuse the same `PostData` struct as /// httpbin returns the received data equally. let json: PostData }
Во-вторых, мы можем использовать полученные данные и декодировать их следующим образом:
/// Use URLSession to fetch the data asynchronously. let (data, response) = try await URLSession.shared.data(for: request) /// Decode the JSON response into the PostResponse struct. let decodedResponse = try JSONDecoder().decode(PostResponse.self, from: data) print("The JSON response contains a name: \(decodedResponse.json.name) and an age: \(decodedResponse.json.age)")
Этот же код работает и для ранее показанного GET-запроса.
Оптимизация обработки ошибок URLSession
До сих пор мы выбрасывали ошибку, если что-то шло не так. Однако мы не проверяли недействительные коды ответов и не использовали преимущества типизированных ошибок. Хотя в обработке ошибок можно пойти на крайние меры, я хотел бы показать пример, в котором мы проверяем код состояния ответа и выбрасываем один тип ошибки, чтобы упростить обработку ошибок на вызывающей стороне:
func performPOSTURLRequest() async throws(NetworkingError) { do { /// Configure the URL for our request. let url = URL(string: "https://httpbin.org/post")! /// Create a URLRequest for the POST request. var request = URLRequest(url: url) /// Configure the HTTP method. request.httpMethod = "POST" /// Configure the proper content-type value to JSON. request.setValue("application/json", forHTTPHeaderField: "Content-Type") /// Define the struct of data and encode it to data. let postData = PostData(name: "Antoine van der Lee", age: 33) let jsonData = try JSONEncoder().encode(postData) /// Pass in the data as the HTTP body. request.httpBody = jsonData /// Use URLSession to fetch the data asynchronously. let (data, response) = try await URLSession.shared.data(for: request) guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { throw NetworkingError.invalidStatusCode(statusCode: -1) } guard (200...299).contains(statusCode) else { throw NetworkingError.invalidStatusCode(statusCode: statusCode) } /// Decode the JSON response into the PostResponse struct. let decodedResponse = try JSONDecoder().decode(PostResponse.self, from: data) print("The JSON response contains a name: \(decodedResponse.json.name) and an age: \(decodedResponse.json.age)") } catch let error as DecodingError { throw .decodingFailed(innerError: error) } catch let error as EncodingError { throw .encodingFailed(innerError: error) } catch let error as URLError { throw .requestFailed(innerError: error) } catch let error as NetworkingError { throw error } catch { throw .otherError(innerError: error) } }
Вы можете обобщить этот код для нескольких запросов, но идея обработки ошибок ясна. Мы отлавливаем конкретные типы ошибок и направляем их в ново определенный NetworkingError
:
enum NetworkingError: Error { case encodingFailed(innerError: EncodingError) case decodingFailed(innerError: DecodingError) case invalidStatusCode(statusCode: Int) case requestFailed(innerError: URLError) case otherError(innerError: Error) }
Этот пример демонстрирует возможности типизированных ошибок и обработки разных вариантов ошибок. Если статус выходит за пределы диапазона от 200 до 299, мы выбрасываем ошибку с недопустимым кодом состояния, которая будет пропущена через операторы catch
. В целом, теперь мы можем сосредоточиться на переборе конкретных типов NetworkingError
на нашей стороне.
Заключение
Современные API Swift в сочетании с URLSession и async/await позволяют написать надежный сетевой слой, не нуждаясь в сторонних зависимостях. В идеале вы должны написать (персональный) SDK, чтобы вы могли повторно использовать свой сетевой слой для любого приложения, которое вы создали (более подробно я объясняю это здесь).