Pull to refresh

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

Development for iOSDevelopment of mobile applicationsSwift
Tutorial
Recovery mode


Сразу хочу сказать, данная статья предназначена прежде всего для новичков. Здесь не будет 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:swiftalamofireiOSios development
Hubs: Development for iOS Development of mobile applications Swift
Total votes 12: ↑11 and ↓1 +10
Views44.8K

Comments 11

Only those users with full accounts are able to leave comments. Log in, please.

Popular right now