
Сразу хочу сказать, данная статья предназначена прежде всего для новичков. Здесь не будет 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.
Выглядеть это должно следующим образом:

Или вот 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 (кнопка с глазом) в дебаг панели:

Примеры
Создание объекта (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, отправкой запросов, обработкой ответа, парснгом положительного ответа, получением информации о прогрессе запроса. Сделали несколько простых запросов и научились загружать фотографии на сервер с авторизацией.
