Pull to refresh

Работа с сервером с помощью Alamofire на Swift

Reading time13 min
Views60K


Сразу хочу сказать, данная статья предназначена прежде всего для новичков. Здесь не будет best practice, создание сервисов, репозиториев и прочей оптимизации кода. Расскажу про основы работы с запросами и покажу применение на примерах.


Содержание



Зачем


Итак, у нас возникла необходимость обрабатывать данные с сервера. Данная задача присутствует почти во всех мобильных приложениях.


Существует нативный инструмент для этого — 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: передается метод запроса и от него зависит, как мы будем общаться с сервером. Чаще всего мы будем:


  1. получать (GET)
  2. изменять (PUT)
  3. отправлять, создавать (POST)
  4. удалять (DELETE)

данные с сервера.


Подробнее про эти и другие методы HTTP можете почитать на википедии:


  1. Протокол HTTP
  2. Методы 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(заголовки) нам будут необходимы в основном для авторизации.


Подробнее про заголовки на википедии:


Заголовки HTTP


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")
    }
}

Подробнее про коды состояний на википедии:


Коды состояния HTTP


Настройка запроса


Для этого у DataRequest есть 4 метода:


  1. validate(statusCode: _ )
  2. validate(contentType: _ )
  3. validate(клоужер для ручной валидации)
  4. validate()

Рассмотрим только последний, потому что его нам будет хватать для 95% запросов.


Взглянем на его реализацию:


public func validate() -> Self {
    return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes)
}

Видим, что он состоит из двух других валидаций:


  1. self.acceptableStatusCodes — возвращает массив статус кодов(Int) из range 200..<300
  2. self.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 в статье на хабре:


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

Tags:
Hubs:
Total votes 12: ↑11 and ↓1+10
Comments11

Articles