Сразу хочу сказать, данная статья предназначена прежде всего для новичков. Здесь не будет best practice, создание сервисов, репозиториев и прочей оптимизации кода. Расскажу про основы работы с запросами и покажу применение на примерах.
Содержание
- Зачем
- Установка
- Настройка доступа HTTP
- Первый минимальный запрос
- Подробнее о минимуме
- Методы HTTP
- Alamofire.request
- Обработка ответа
- Обработка результата ответа
- Разные типы ответов
- Прогресс загрузки
- Примеры
- Итог
Зачем
Итак, у нас возникла необходимость обрабатывать данные с сервера. Данная задача присутствует почти во всех мобильных приложениях.
Существует нативный инструмент для этого — URLSession, но работать с ним немного сложнее, чем хотелось бы. Для облегчения этого процесса существует framework Alamofire — это обвертка над URLSession, которая сильно упрощает жизнь при работе с сервером.
Установка
Воспользуемся CocoaPods т.к. с ним очень легко и быстро работать.
Добавим в Podfile:
pod 'Alamofire'
Для использования Alamofire версии 4+ необходимы следующие требования:
- iOS 9.0+ / macOS 10.11+ / tvOS 9.0+ / watchOS 2.0+
- Xcode 8.0+
- Swift 3.0+
- CocoaPods 1.1.0+
Так же нам необходимо добавить use_frameworks!
.
Так будет выглядеть минимальный Podfile:
platform :ios, '9.0'
use_frameworks!
target 'Networking' do
pod 'Alamofire'
end
Настройка доступа HTTP
По умолчанию в приложении закрыт доступ к HTTP соединениям, доступны только HTTPS. Но пока еще очень много сайтов не перешли на https.
Мы будем работать с сервером http://jsonplaceholder.typicode.com, а он работает по http. Поэтому нам надо открыть доступ для него.
Для тренировки мы откроем доступ для всех сайтов. Открытие для одного сайта в данной статье не буду рассматривать.
Открываем Info.plist и добавляем в него App Transport Security Settings и внутрь этого параметра необходимо добавить Allow Arbitrary Loads, со значением YES.
Выглядеть это должно следующим образом:
![Info.plist](/Users/zdaecqzezdaecq/Downloads/Работа с запросам с помощью Alamofire/info_plist.png)
Или вот Source code, который необходимо добавить:
правой кнопкой мыши на Info.plist -> Open as -> Source code
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Первый минимальный запрос
Открываем проект.
Не забудьте, что нам нужно открыть Networking.xcworkspace, а не Networking.xcodeproj, который создался после pod install
Открываем файл ViewController.swift и заменяем его код на следующий:
import UIKit
import Alamofire
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
request("http://jsonplaceholder.typicode.com/posts").responseJSON { response in
print(response)
}
print("viewDidLoad ended")
}
}
Запускайте проект.
В консоли выведится:
viewDidLoad ended
SUCCESS: (
{
body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto";
id = 1;
title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit";
userId = 1;
},
...
Поздравляю! Вы сделали первый запрос на сервер и получили от него ответ с результатом.
Подробнее о минимуме
Нам необходимо получить доступ к новым функция, т.к. это не наши файлы, а отдельная библиотека:
import Alamofire
Собственно сам метод запроса:
request
Далее первым параметром передается URL, по которому будет производится запрос:
"http://jsonplaceholder.typicode.com/posts"
Метод responseJSON
говорит о том, что ответ от сервера нам нужен в JSON формате.
Далее в клоужере мы получаем ответ от сервера и выводим его в консоль:
{ response in
print(response)
}
Важно заметить, что код в этом клоужере происходит асинхронно и выполнится после выхода из viewDidLoad, тем самым строка viewDidLoad ended
в консоль выводится раньше.
Методы HTTP
На самом деле мы сделали GET запрос, но нигде этого не указывали. Начиная с Alamofire 4 по умолчанию выполняется GET запрос. Мы может его явно указать, заменив соответствующий код на следующий:
request("http://jsonplaceholder.typicode.com/posts", method: .get)
Как Вы уже поняли в параметре method: передается метод запроса и от него зависит, как мы будем общаться с сервером. Чаще всего мы будем:
- получать (GET)
- изменять (PUT)
- отправлять, создавать (POST)
- удалять (DELETE)
данные с сервера.
Подробнее про эти и другие методы HTTP можете почитать на википедии:
Alamofire.request
Функция request
— глобальная функция, поэтому мы можем ее вызывать через Alamofire.request
или просто request
.
Так выглядит полный запрос со всеми параметрами:
request(URLConvertible, method: HTTPMethod, parameters: Parameters?, encoding: ParameterEncoding, headers: HTTPHeaders?)
Рассмотрим подробнее:
URLConvertible
Первым параметром является путь запросу и он принимает URLConvertible
. (Ваш КЭП)
Если мы посмотрим на его реализацию, то увидим, что это протокол с одной функцией:
public protocol URLConvertible {
func asURL() throws -> URL
}
и он уже реализован для следующих типов данных:
- String
- URL
- URLComponents
В связи с этим мы можем передавать любой из выше перечисленных типов в качестве параметра или создать свой собственный тип данных с реализацией данного протокола.
HTTPMethod
Это enum, со всеми возможными типами запросов:
public enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
Как мы уже выяснили: по умолчанию .get
Тут ничего сложного, идем дальше.
Parameters
Это простой Dictionary:
public typealias Parameters = [String: Any]
Через параметры мы будем передавать данные на сервер (например, для изменения или создания объектов).
ParameterEncoding
Это тоже протокол с одной функцией:
public protocol ParameterEncoding {
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
Он необходим для определения в каком виде нам закодировать наши параметры. Разные серверы и запросы требуют определенной кодировки.
Этот протокол реализуют:
- URLEncoding
- JSONEncoding
- PropertyListEncoding
По умолчанию у нас URLEncoding.default
.
В основном этот параметр не используется, но иногда бывает нужен, в частности JSONEncoding.default
для кодировки в JSON формате и PropertyListEncoding.default
в XML.
Я заметил, что Int
не отправляется без JSONEncoding.default
, но возможно это было в Alamofire 3, а может из-за сервера. Просто имейте это ввиду.
HTTPHeaders
Это также Dictionary, но другой типизации:
public typealias HTTPHeaders = [String: String]
Headers(заголовки) нам будут необходимы в основном для авторизации.
Подробнее про заголовки на википедии:
DataRequest
На выходе мы получаем объект типа DataRequest
— сам запрос. Его мы можем сохранить, передать, как параметр в другую функцию при необходимости, донастроить и отправить. Об этом далее.
Обработка ответа
Ответ от сервера может прийти, как с результатом, так и с ошибкой. Для того, чтобы их различать у ответа есть такие параметры, как statusCode
и contentType
.
Вот эти параметры мы можем проверять вручную, а можем настроить запрос, так что он нам сразу будет говорить, ответ пришел с ошибкой или с результатом.
Ручная обработка ответа
Если мы не настраивали валидацию, то в
responseJSON.response?.statusCode
у нас будет статус код ответа, а в
responseJSON.result.value
будет результат, если ответ пришел без ошибки, и в
responseJSON.result.error
если с ошибкой.
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in
guard let statusCode = responseJSON.response?.statusCode else { return }
print("statusCode: ", statusCode)
if (200..<300).contains(statusCode) {
let value = responseJSON.result.value
print("value: ", value ?? "nil")
} else {
print("error")
}
}
Подробнее про коды состояний на википедии:
Настройка запроса
Для этого у DataRequest
есть 4 метода:
- validate(statusCode: _ )
- validate(contentType: _ )
- validate(клоужер для ручной валидации)
- validate()
Рассмотрим только последний, потому что его нам будет хватать для 95% запросов.
Взглянем на его реализацию:
public func validate() -> Self {
return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
}
Видим, что он состоит из двух других валидаций:
self.acceptableStatusCodes
— возвращает массив статус кодов(Int) из range 200..<300self.acceptableContentTypes
— возвращает массив допустимых хедеров(String)
У DataResponse
есть параметр result
, который может сказать нам, пришел ответ с ошибкой или с результатом.
Итак, применим валидацию для запроса:
request("http://jsonplaceholder.typicode.com/posts").validate().responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
}
Если у нас не будет вылидации запроса (validate()
), то result
всегда будет равен .success
, за исключением ошибки из-за отсутствия интернета.
Можно обрабатывать ответ обоими способами, но я настоятельно рекомендую пользоваться настройкой валидации запроса — будет меньше ошибок!
Обработка результата ответа
Ответ от сервера чаще всего бывает в виде одного объекта или массива объектов.
Если мы посмотрим на тип результата ответа, то увидим тип Any
. Чтобы из него что-то достать — нам надо его привести к нужному формату.
В логах мы замечали, что у нас приходит массив Dictionary, поэтому к нему и будем приводить:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
print("value", value)
guard let jsonArray = responseJSON.result.value as? [[String: Any]] else { return }
print("array: ", jsonArray)
print("1 object: ", jsonArray[0])
print("id: ", jsonArray[0]["id"]!)
case .failure(let error):
print(error)
}
}
После этого, как показано выше, мы можем делать что угодно, например, создать объект и сохранить его, чтобы потом было удобнее работать с данными.
В отдельном файле создадим структуру Post:
struct Post {
var id: Int
var title: String
var body: String
var userId: String
}
Так будет выглядеть парсинг в массив объектов:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
guard let jsonArray = value as? Array<[String: Any]> else { return }
var posts: [Post] = []
for jsonObject in jsonArray {
guard
let id = jsonObject["id"] as? Int,
let title = jsonObject["title"] as? String,
let body = jsonObject["body"] as? String,
let userId = jsonObject["userId"] as? String
else {
return
}
let post = Post(id: id, title: title, body: body, userId: userId)
posts.append(post)
}
print(posts)
case .failure(let error):
print(error)
}
}
Парсинг объекта внутри запроса выглядит очень плохо + нам придется всегда копировать эти строки для каждого запроса. Чтобы от этого избавиться создадим конструктор init?(json: [String: Any])
:
init?(json: [String: Any]) {
guard
let id = json["id"] as? Int,
let title = json["title"] as? String,
let body = json["body"] as? String,
let userId = json["userId"] as? String
else {
return nil
}
self.id = id
self.title = title
self.body = body
self.userId = userId
}
Он может вернуть nil, если сервер нам что-то не вернул
И тогда метод запроса выглядит на много понятнее и приятнее:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
guard let jsonArray = value as? Array<[String: Any]> else { return }
var posts: [Post] = []
for jsonObject in jsonArray {
guard let post = Post(json: jsonObject) else { return }
posts.append(post)
}
print(posts)
case .failure(let error):
print(error)
}
}
Пойдем еще дальше и в Post добавим метод обработки массива:
static func getArray(from jsonArray: Any) -> [Post]? {
guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil }
var posts: [Post] = []
for jsonObject in jsonArray {
if let post = Post(json: jsonObject) {
posts.append(post)
}
}
return posts
}
Тогда метод запроса примет следующий вид:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
guard let posts = Post.getArray(from: value) else { return }
print(posts)
case .failure(let error):
print(error)
}
}
Конечный вариант файла Post.swift:
import Foundation
struct Post {
var id: Int
var title: String
var body: String
var userId: String
init?(json: [String: Any]) {
guard
let id = json["id"] as? Int,
let title = json["title"] as? String,
let body = json["body"] as? String,
let userId = json["userId"] as? String
else {
return nil
}
self.id = id
self.title = title
self.body = body
self.userId = userId
}
static func getArray(from jsonArray: Any) -> [Post]? {
guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil }
var posts: [Post] = []
for jsonObject in jsonArray {
if let post = Post(json: jsonObject) {
posts.append(post)
}
}
return posts
}
}
Для тех кто уже разобрался в работе с flatMap, то функцию getArray
можно написать так:
static func getArray(from jsonArray: Any) -> [Post]? {
guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil }
return jsonArray.flatMap { Post(json: $0) }
}
Разные типы ответов
responseJSON
Как отправлять запрос и получать ответ в виде JSON с помощью responseJSON мы научились. Теперь разберем в каком еще виде можем получить ответ.
responseData
Ответ нам придет в виде Data. Зачастую так приходят картинки, но даже наш предыдущий запрос мы можем получть в виде Data:
request("http://jsonplaceholder.typicode.com/posts").responseData { responseData in
switch responseData.result {
case .success(let value):
guard let string = String(data: value, encoding: .utf8) else { return }
print(string)
case .failure(let error):
print(error)
}
}
В примере мы получает ответ и преобразовываем его в строку. Из нее неудобно получать данные, как из Dictionary, но есть парсеры, которые сделают из стоки объект.
responseString
Здесь все просто. Ответ придет в виде JSON строки. По факту он делает, то, что мы написали выше в responseData:
request("http://jsonplaceholder.typicode.com/posts").responseString { responseString in
switch responseString.result {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
}
response
Можно сказать это базовый метод. Он никак не обрабатывает данные от сервера, выдает их в том виде, в каком они пришли. У него нету свойства result
и поэтому конструкция вида switch response.result
здесь не сработает. Все придется делать вручную. Он нам редко понадобится, но знать о нем надо.
request("http://jsonplaceholder.typicode.com/posts").response { response in
guard
let data = response.data,
let string = String(data: data, encoding: .utf8)
else { return }
print(string)
}
Выведется строка, если ответ пришел без ошибки.
responsePropertyList
Существует еще метод .responsePropertyList
. Он нужен для получения распарсенного plist файла. Я им еще не пользовался и не нашел тестого сервера, чтобы привести пример. Просто знайте, что он есть или можете сами с ним разобраться по аналогии с другими.
Прогресс загрузки
Иногда мы можем получать большой ответ от сервера, например, когда скачиваем фотографию, и нам необходимо отображать прогресс загрузки. Для этого у request есть метод downloadProgress
:
Вместо https://s-media-cache-ak0.pinimg.com/originals/ef/6f/8a/ef6f8ac3c1d9038cad7f072261ffc841.jpg
можете вставить любую ссылку на фотографию. Желательно большую, чтобы запрос не выполнился моментально и вы увидели сам процесс.
request("https://s-media-cache-ak0.pinimg.com/originals/ef/6f/8a/ef6f8ac3c1d9038cad7f072261ffc841.jpg")
.validate()
.downloadProgress { progress in
print("totalUnitCount:\n", progress.totalUnitCount)
print("completedUnitCount:\n", progress.completedUnitCount)
print("fractionCompleted:\n", progress.fractionCompleted)
print("localizedDescription:\n", progress.localizedDescription)
print("---------------------------------------------")
}
.response { response in
guard
let data = response.data,
let image = UIImage(data: data)
else { return }
print(image)
}
Класс Progress
— это класс стандартной библиотеки.
В логах будет выводиться прогресс в виде блоков:
totalUnitCount:
2113789
completedUnitCount:
2096902
fractionCompleted:
0.992011028536907
localizedDescription:
99% completed
Мы можем поделить completedUnitCount на totalUnitCount и получим число от 0 до 1, которое будет использоваться в UIProgressView, но за нас это уже сделали в свойстве fractionCompleted.
Чтобы увидеть саму картинку, поставьте breakpoint на строку с print(image)
и нажмите на Quick Look (кнопка с глазом) в дебаг панели:
![Debug console](/Users/zdaecqzezdaecq/Downloads/Работа с запросам с помощью Alamofire/image_quick_look.png)
Примеры
Создание объекта (POST)
Самое простое создание объекта на сервере выглядит так:
let params: [String: Any] = [
"title": "new post",
"body": "some news",
"userId": 10
]
request("http://jsonplaceholder.typicode.com/posts", method: .post, parameters: params).validate().responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
guard
let jsonObject = value as? [String: Any],
let post = Post(json: jsonObject)
else { return }
print(post)
case .failure(let error):
print(error)
}
}
id не передаем т.к. сервер должен сам его назначить. А вообще для создания каждого объекта в документации должны прописываться необходимые параметры.
Обновление объекта (PUT)
При обновлении объекта, его id зачастую прописывается не в параметре, а в пути запроса (~/posts/1
):
let params: [String: Any] = [
"title": "new post",
"body": "some news",
"userId": 10
]
request("http://jsonplaceholder.typicode.com/posts/1", method: .put, parameters: params).validate().responseJSON { responseJSON in
switch responseJSON.result {
case .success(let value):
guard
let jsonObject = value as? [String: Any],
let post = Post(json: jsonObject)
else { return }
print(post)
case .failure(let error):
print(error)
}
}
Конечно, могут сделать и через параметр, но это будет не по REST. Подробнее про REST в статье на хабре:
Загрузка фотографии на сервер (multipartFormData)
Так выглядит загрузка фотографии на сервер:
let image = UIImage(named: "some_photo")!
let data = UIImagePNGRepresentation(image)!
let httpHeaders = ["Authorization": "Basic YWNjXzE4MTM2ZmRhOW*****A=="]
upload(multipartFormData: { multipartFormData in
multipartFormData.append(data, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg")
}, to: "https://api.imagga.com/v1/content", headers: httpHeaders, encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let uploadRequest, let streamingFromDisk, let streamFileURL):
print(uploadRequest)
print(streamingFromDisk)
print(streamFileURL ?? "streamFileURL is NIL")
uploadRequest.validate().responseJSON() { responseJSON in
switch responseJSON.result {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
}
case .failure(let error):
print(error)
}
})
Ужасно не правда ли?
Давайте разберем, что за что отвечает.
Я закинул фотографию с именем some_photo в Assets.xcassets
Создаем объект картинки и преобразуем ее в Data:
let image = UIImage(named: "some_photo")!
let data = UIImagePNGRepresentation(image)!
Создаем словарь для передачи токена авторизации:
let httpHeaders = ["Authorization": "Basic YWNjXzE4MTM2ZmRhOW*****A=="]
Это необходимо т.к. сервис www.imagga.com требует авторизацию, чтобы залить картинку.
Чтобы получить свой токен, вам необходимо всего лишь зарегистрироваться на их сайте и скопировать его из своего профиля по ссылке: https://imagga.com/profile/dashboard
До этого мы использовали метод request
. Сдесь же используется метод upload
. Первым параметром идет клоужер для присоединения нашей картинки:
upload(multipartFormData: { multipartFormData in
multipartFormData.append(data, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg")
}
Следующими параметрами идут URL и headers:
to: "https://api.imagga.com/v1/content", headers: httpHeaders
Дальше идет клоужер с закодированным запросом:
encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let uploadRequest, let streamingFromDisk, let streamFileURL):
print(uploadRequest)
print(streamingFromDisk)
print(streamFileURL ?? "streamFileURL is NIL")
...
case .failure(let error):
print(error)
}
})
Из него мы можем получить запрос (uploadRequest), и две переменные необходимые для потока(stream) файлов.
Про потоки говорить не буду, достаточно редкая штука. Пока вы просто увидите, что эти две переменные равны false и nil соответственно.
Дальше мы должны отправить запрос в привычной для нас форме:
uploadRequest.validate().responseJSON() { responseJSON in
switch responseJSON.result {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
}
Когда вы получите свой токен, вставите свою фотографию и выполните запрос, то результат будет следующим:
{
status = success;
uploaded = (
{
filename = "image.jpg";
id = 83800f331a7f97e41e0f0b70bf7847bd;
}
);
}
filename может не отличаться, а id будут.
Итог
Мы познакомились с фреймворком Alamofire, разобрались с методом request, отправкой запросов, обработкой ответа, парснгом положительного ответа, получением информации о прогрессе запроса. Сделали несколько простых запросов и научились загружать фотографии на сервер с авторизацией.