Хай! Вам знакомо то чувство уныния, когда нужно интегрировать проект с очередным RESTful API? Это когда в очередной раз нужно создавать какой-нибудь APIManager и наполнять его Alamofire запросами, а потом связывать их с моделями маппинга данных. Лично я стараюсь максимально оптимизировать всю свою работу, поэтому регулярно изучаю различные библиотеки чтобы не писать кучу повторяющегося кода и избавиться от рутины. В один из таких заходов я наткнулся на отличную библиотеку Moya, о которой и пойдёт речь.
Первое знакомство
На самом деле, на эту библиотеку я натыкался несколько раз и она даже пылилась у меня в закладках браузера, но откладывал её изучение, о чём впоследствии не раз пожалел. Авторы этой библиотеки выложили красноречивую картинку «до» и «после» в своём репозитории:
Впечатляет, правда? Суть библиотеки сводится к тому, что всю сетевую часть можно интегрировать быстро и минимальными телодвижениями — всю низкоуровневую работу за вас сделает Moya.
Начинаем интеграцию
Создадим Single-View Applicaton и и подключим библиотеку к нашему проекту (для маппинга я предпочитаю библиотеку ObjectMapper, для подключения сторонних зависимостей — CocoaPods)
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. Лично я предпочитаю последнюю библиотеку, поэтому мой пример будет для неё. Для начала давайте добавим нужные зависимости:
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:
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.
Автор: Святослав