Moya — как перестать беспокоиться о сетевой части и начать жить

в 16:40, , рубрики: ios development, moya, swift, разработка под iOS

Хай! Вам знакомо то чувство уныния, когда нужно интегрировать проект с очередным RESTful API? Это когда в очередной раз нужно создавать какой-нибудь APIManager и наполнять его Alamofire запросами, а потом связывать их с моделями маппинга данных. Лично я стараюсь максимально оптимизировать всю свою работу, поэтому регулярно изучаю различные библиотеки чтобы не писать кучу повторяющегося кода и избавиться от рутины. В один из таких заходов я наткнулся на отличную библиотеку Moya, о которой и пойдёт речь.

Первое знакомство

На самом деле, на эту библиотеку я натыкался несколько раз и она даже пылилась у меня в закладках браузера, но откладывал её изучение, о чём впоследствии не раз пожалел. Авторы этой библиотеки выложили красноречивую картинку «до» и «после» в своём репозитории:
image
Впечатляет, правда? Суть библиотеки сводится к тому, что всю сетевую часть можно интегрировать быстро и минимальными телодвижениями — всю низкоуровневую работу за вас сделает Moya.

Начинаем интеграцию

Создадим Single-View Applicaton и и подключим библиотеку к нашему проекту (для маппинга я предпочитаю библиотеку ObjectMapper, для подключения сторонних зависимостей — CocoaPods)

Podfile

platform :ios, '9.0'

def app_pods
pod 'ObjectMapper', '~> 2.2'
pod 'Moya'
pod 'Moya-ObjectMapper'
end

target 'MoyaExample' do
use_frameworks!
app_pods

# Pods for MoyaExample

target 'MoyaExampleTests' do
inherit! :search_paths
app_pods
end

end

Далее нам нужно создать файл с запросами, делается это так:

import Moya

enum MoyaExampleService {
    case getRestaurants(page: Int?, perPage: Int?)
}

extension MoyaExampleService: TargetType {
    var baseURL: URL { return URL(string: "http://moya-example.svyatoslav-reshetnikov.ru")! }
    var path: String {
        switch self {
        case .getRestaurants:
            return "/restaurants.json"
    }
    var method: Moya.Method {
        return .get
    }
    var parameters: [String: Any]? {
        return nil
    }
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    var sampleData: Data {
        return Data()
    }
    var task: Task {
        return .request
    }
}

В этом файле происходит настройка запросов. В самом начале мы видим enum — это наш будущий сервис со всеми запросами. Можно запихнуть все запросы в один сервис, но в больших проектах я рекомендую придерживаться буквы I из SOLID и не превращать файл в кашу. После перечисления всех запросов в enum нам нужно расширить класс протоколом TargetType. Давайте рассмотрим подробней содержание этого протокола:

1. var baseURL — это адрес сервера, на котором лежит RESTful API.
2. var path — это роуты запросов.
3. var method — это метод, который мы хотим послать. Moya ничего не придумывает и берёт все методы из Alamofire.
4. var parameters — это параметры запроса. На данном этапе библиотеку не волнует будут ли эти параметры в теле запроса (POST) или в url (GET), эти нюансы определяются позже. Пока просто пишем параметры, которые мы хотим передать в запросе.
5. var parameterEncoding — это кодировка параметров, также берётся из Alamofire. Можно сделать их как json, можно как url, можно как property list.
6. var sampleData — это так называемые stubs, используются для тестирования. Можно взять стандартный ответ от сервера, сохранить его в проекте в формате JSON и затем использовать в unit тестах.
7. var task — это задача, которую мы будем выполнять. Их всего 3 — request, download и upload.

Применяем в проекте

Для того, чтобы начать использовать Moya, нам необходимо создать Provider — это абстракция библиотеки, которая даёт доступ к запросам:

let provider = MoyaProvider<MoyaExampleService>()

После этого можно жениться делать запрос с помощью provider:

provider.request(.getRestaurants()) { result in
    switch result {
    case .success(let response):
        let restaurantsResponse = try? response.mapObject(RestaurantsResponse.self)
        // Do something with restaurantsResponse
    case .failure(let error):
        print(error.errorDescription ?? "Unknown error")
    }
}

Добавляем реактивности

Moya поддерживает ReactiveSwift и RxSwift. Лично я предпочитаю последнюю библиотеку, поэтому мой пример будет для неё. Для начала давайте добавим нужные зависимости:

Podfile

platform :ios, '9.0'

def app_pods
pod 'ObjectMapper', '~> 2.2'
pod 'Moya'
pod 'Moya-ObjectMapper'
pod 'Moya/RxSwift'
pod 'Moya-ObjectMapper/RxSwift'
end

target 'MoyaExample' do
use_frameworks!
app_pods

# Pods for MoyaExample

target 'MoyaExampleTests' do
inherit! :search_paths
app_pods
end

end

И наш код трансформируется следующим образом:

let provider = RxMoyaProvider<MoyaExampleService>()
    provider.request(.getRestaurants())
        .mapObject(RestaurantsResponse.self)
        .catchError { error in
            // Do something with error
            return Observable.error(error)
        }
        .subscribe(
            onNext: { response in
                self.restaurants = response.data
            }
        )
        .addDisposableTo(disposeBag)

Пара трюков с Moya

Обо всех возможностях Moya рассказывать долго, поэтому советую после прочтения этой статьи заглянуть в документацию. А я сейчас покажу несколько вещей, которые могут пригодиться и Вам:

1. Добавить что-нибудь в заголовок запроса (например, basic auth)
Сначала сделаем requestClosure — это замыкание, в котором мы можем модифицировать отправляемый запрос:

let requestClosure = { (endpoint: Endpoint<MoyaExampleService>, done: MoyaProvider.RequestResultClosure) in
    var request = endpoint.urlRequest
    request?.setValue("set_your_token", forHTTPHeaderField: "XAuthToken")
    done(.success(request!))
}

Этот requestClosure надо обязательно добавить в provider:

let provider = RxMoyaProvider<MoyaExampleService>(requestClosure: requestClosure)

2. Продебажить запрос

В Moya есть крутая штука — плагины, советую изучить их поподробней. Один из плагинов, например, автоматически выводит в консоль все ваши запросы:

let provider = RxMoyaProvider<MoyaExampleService>(plugins: [NetworkLoggerPlugin(verbose: true)])

Unit тесты

Я предпочитаю BDD стиль тестов, поэтому для unit-тестирования будем использовать библиотеки Quick и Nimble. Добавим их в наш Podfile:

Podfile

platform :ios, '9.0'

def app_pods
pod 'ObjectMapper', '~> 2.2'
pod 'Moya'
pod 'Moya-ObjectMapper'
pod 'Moya/RxSwift'
pod 'Moya-ObjectMapper/RxSwift'
end

def test_pods
pod 'Quick'
pod 'Nimble'
end

target 'MoyaExample' do
use_frameworks!
app_pods

# Pods for MoyaExample

target 'MoyaExampleTests' do
inherit! :search_paths
app_pods
test_pods
end

end

Теперь пишем небольшой тест:

import Quick
import Nimble
import RxSwift
import Moya
@testable import MoyaExample

class NetworkTests: QuickSpec {
    
    override func spec() {
        var testProvider: RxMoyaProvider<MoyaExampleService>!
        let disposeBag = DisposeBag()
        
        beforeSuite {
            testProvider = RxMoyaProvider<MoyaExampleService>(stubClosure: MoyaProvider.immediatelyStub)
        }
        
        describe("testProvider") {
            it("should be not nil") {
                expect(testProvider).toNot(beNil())
            }
        }
        
        describe("getRestaurants") {
            it("should return not nil RestaurantsResponse object") {
                testProvider.request(.getRestaurants())
                    .mapObject(RestaurantsResponse.self)
                    .subscribe(
                        onNext: { response in
                            expect(response).toNot(beNil())
                    }
                    )
                    .addDisposableTo(disposeBag)
            }
        }
    }
}

Запускаем тесты, убеждаемся что они пройдены, после чего убеждаемся что сетевая часть покрыта тестами на 100% (как включить code coverage в xcode читайте здесь).

Заключение

В этой статье я хотел дать читателям базовое представление о мощной сетевой библиотеке Moya, намеренно опустив нюансы для того, чтобы вы самостоятельно смогли исследовать её и насладиться развитым инструментарием, который позволяет решать широкий спектр задач при выстраивании сетевого слоя в iOS разработке. Исходный код ждёт Вас на Github.

Автор: Святослав

Источник

Поделиться

* - обязательные к заполнению поля