Хай! Вам знакомо то чувство уныния, когда нужно интегрировать проект с очередным 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.
Поделиться с друзьями
-->

Комментарии (10)


  1. rule
    07.07.2017 07:20

    Почитал очереднюю статью про Moya.
    И так не вижу вообще плюсов особых, вообще не убедительно. Ну и реактивность тут еще и пру строк кода добавила. Зачем она? Потому-что модно?
    Просто навязывает одно архитектурное решение.


    1. svyat_reshetnikov
      07.07.2017 10:52

      Почитал очереднюю статью про Moya.

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

      Как говорится, не нравится — не ешьте. Я не евангелист этой библиотеки, моя задача — показать что такое решение есть и объяснить как с ним работать, использовать его или нет — дело Ваше.
      Ну и реактивность тут еще и пру строк кода добавила. Зачем она? Потому-что модно?

      Реактивность хорошо подходит для своих задач, например биндинг данных или избегание callback-hell при вызове двух и более сетевых запросов. Если Вам нравится completion, который вложен в completion, который вложен в completion — это Ваше право. Мне вот не нравится, поэтому и использую RxSwift.
      Просто навязывает одно архитектурное решение.

      Реактивность частенько готовят с MVVM, я даже писал про это статью, но в целом это самодостаточная библиотека, которую можно использовать с любой архитектурой. В своих проектах я придерживаюсь архитектуры VIPER и RxSwift туда очень хорошо вписывается, поэтому про навязывание архитектурного решения Вы погорячились.


      1. rule
        08.07.2017 11:18

        Как говорится, не нравится — не ешьте. Я не евангелист этой библиотеки, моя задача — показать что такое решение есть и объяснить как с ним работать, использовать его или нет — дело Ваше.

        Грубо. Но я не нежный. Вообще то вы утверждаете первым предложением: «Moya — как перестать беспокоиться о сетевой части и начать жить». Потом не привели ни одного убедительного доказательства того, что беспокоится больше не нужно. А на мой вопрос «а как же ?» отвечаете: «Не нравится — не ешьте». Как по мне не последовательно, могу ответить в вашем духе: «Не нравится критика — не пишите публичные статьи».

        Реактивность хорошо подходит для своих задач, например биндинг данных или избегание callback-hell при вызове двух и более сетевых запросов.

        Да кто спорит, но мы же рассматриваем конкрентный случай. В вашем примере никаких абсолютно плюсов нет, а читатель же должен перестать беспокоится почему-то.

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


        1. svyat_reshetnikov
          09.07.2017 14:09
          -1

          В мыслях не было грубить. Интернет, к сожалению, не передаёт эмоциональную составляющую слов. «Не нравится — не ешьте» это, скорее, безразличие, но никак не грубость.

          Меня искренне смущает, что у Вас в профиле написано

          Я разработчик с многолетним стажем
          но при этом Вы в упор не видите преимуществ этой библиотеки. Что ж, придётся мне это сделать за Вас.

          Для начала идём в репозиторий библиотеки Moya и смотрим что там написали авторы. Я напишу сразу перевод:
          Специальные сетевые уровни, которые распространены в iOS, плохи по нескольким причинам:
          — Сложно писать новые приложения («С чего мне начать?»)
          — Сложно поддерживать существующие приложения («О. Господи, такой беспорядок...»)
          — Сложно писать unit-тесты («Как мне сделать это снова?»)

          Итак, основная идея Moya заключается в том, что мы хотим, чтобы уровень сетевой абстракции был достаточно инкапсулирован, фактически вызывая Alamofire напрямую. Это должно быть достаточно простым, чтобы можно было легко сделать базовые вещи, но достаточно всеобъемлющим, чтобы сложные вещи также можно было легко сделать.

          Если вы используете Alamofire для того, чтобы абстрагироваться от URLSession, то почему бы не использовать что-то, чтобы абстрагироваться от URL-адресов, параметров и т.д.?

          Некоторые крутые фичи в Moya:
          — Проверка при компиляции для корректного доступа к API endpoints (не знаю как лучше перевести «Compile-time checking for correct API endpoint accesses.», если есть более «русский» вариант — предлагайте)
          — Позволяет определить четкое использование разных endpoints с соответствующими значениями enums.
          — Обрабатывает тестовые заглушки как объекты первого класса, таким образом unit-тестирование становится очень лёгким.
          Из своего личного опыта могу сказать, что скорость интеграции сетевой части с помощью Moya лично в моей работе увеличилась в разы. И тесты стало писать гораздо приятней.

          Что касается этого:
          По поводу навязанной архитектуры, я имел ввиду архитектуру построения Moya. Он жестко диктует условия построения API слоя
          А в чём, собственно, заключается эта «жёсткость»? Вы можете свободно манипулировать роутами, параметрами, методами, задачами, модифицировать вход и выход запроса, создавать свои собственные stubs для тестирования, даже писать свои плагины и использовать уже готовые решения. Если Вы про то, что нужно создавать т.н. файл с запросами по определённым правилам, то позвольте спросить — а что в этом плохого? Как по мне, так архитектура выстроена очень здорово — ничего лишнего и все сделано очень удобно. Так что я искренне не понимаю почему Вы считаете что это минус, при том условии, что Вам выдаются все инструменты для манипуляций.

          Продолжая тему RxSwift
          Да кто спорит, но мы же рассматриваем конкрентный случай. В вашем примере никаких абсолютно плюсов нет
          А Вы сами как думаете, если я создам ещё один запрос и захочу вызвать 2 запроса подряд, то как это будет выглядеть нереактивно и реактивно? Очевидно что в нереактивном случае будет callback-hell:
          provider.request(.getRestaurants()) { result in
              switch result {
              case .success(let response):
                  let restaurantsResponse = try? response.mapObject(RestaurantsResponse.self)
                  // Do something with restaurantsResponse
                  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")
                      }
              case .failure(let error):
                  print(error.errorDescription ?? "Unknown error")
              }
          }
          

          Теперь посмотрим как это будет реактивно:
          let provider = RxMoyaProvider<MoyaExampleService>()
              provider.request(.getRestaurants())
                  .mapObject(RestaurantsResponse.self)
                  .flatMap {
                      return 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, сравнил с Alamofire и не получил абсолютно никакого выигрыша ни в скорости интеграции сетевой части, ни в скорости написания unit-тестов», то для меня это была бы интересная дискуссия. А по Вашим комментариям складывается такое чувство, что Вы пробежались по статье, не нашли того что искали — неопровержимых доказательств необходимости немедленного перехода на Moya — и принялись критиковать. Проблема в том, что статью я писал не для того, чтобы кому-то что-то доказать, а для того, чтобы показать что есть такой инструмент, который лично мне очень сильно помогает в работе.

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

          Начнём с того, что я обыграл название книги Дейла Карнеги «Как перестать беспокоиться и начать жить». Просто потому что мне захотелось так назвать свою статью. Относитесь к этому попроще что ли, перестаньте беспокоиться — и жить сразу станет легче и приятней)

          Удачи!


          1. rule
            09.07.2017 14:27
            -1

            Из своего личного опыта могу сказать, что скорость интеграции сетевой части с помощью Moya лично в моей работе увеличилась в разы. И тесты стало писать гораздо приятней.

            Так это может вы просто плохо без Maya писали.
            Цитировать и переводить документацию было лишния, я умею читать и ходить по ссылкам.
            Все достопремичательности что вы привели — это бла, бла, бла, смотрте как надо. А я не вижу в упор что именно это улучшает.

            Навязанное архитектурное решение — это жестко следовать предолженному протоколу и организации коду, который сваливает в кучу все запросы, а если у меня не монолитный сервис и всё таки нжуно поддержать зависимости между вызовами вся эта структура рушится.

            Есть альтернативные подходы, типа NSOperation для каждого запроса с зависимостями ( и не реактивность не понадобится и лапши с колбэками не будет).
            Или просто самому Enum написать с запросами в Alamofiore? Что решает Moya не понятно, то что. у них написано в примерах можно с такими-же усилиями и стандартными средствами сделать.

            Вам больше какой вариант нравится? Я предпочитаю реактивный вариант.

            Мне оба варианта не нравятся.

            Проблема в том, что статью я писал не для того, чтобы кому-то что-то доказать, а для того, чтобы показать что есть такой инструмент, который лично мне очень сильно помогает в работе.

            Ну тогда ей место в личном бложике. На хабре вроде принято остаивать свою точку зрения и техническое решение.

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


            1. svyat_reshetnikov
              09.07.2017 15:28

              Так это может вы просто плохо без Maya писали.
              Вот это уже звучит как переход на личности — если моя точка зрения не совпадает с Вашей, то упрекать меня в непрофессионализме как минимум не этично.

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

              Навязанное архитектурное решение — это жестко следовать предолженному протоколу и организации коду, который сваливает в кучу все запросы, а если у меня не монолитный сервис и всё таки нжуно поддержать зависимости между вызовами вся эта структура рушится.
              Букву I из SOLID никто не отменял. Ничего не мешает Вам создать несколько сервисов, я сам против того чтобы все запросы сваливать в одну кучу. Совсем не вижу проблемы, которую Вы описали.

              Есть альтернативные подходы, типа NSOperation для каждого запроса с зависимостями ( и не реактивность не понадобится и лапши с колбэками не будет).
              Или просто самому Enum написать с запросами в Alamofiore? Что решает Moya не понятно, то что. у них написано в примерах можно с такими-же усилиями и стандартными средствами сделать.
              Вы можете привести пример кода? Если Вы утверждаете, что можно интегрировать сетевую часть так же быстро и просто, а также легко протестировать, и всё это с помощью стандартных средств — я буду только рад узнать что-то новое.

              Ну тогда ей место в личном бложике. На хабре вроде принято остаивать свою точку зрения и техническое решение.
              Давайте вы не будете указывать мне, где мне публиковать свои статьи. Я взрослый человек и сам в состоянии решить этот вопрос. То, что для меня звучит убедительно, для Вас звучит неубедительно — это нормальная ситуация. Всё, что я смог — я сказал, убеждать Вас в мои обязанности не входит — мне за это не платят.


              1. rule
                09.07.2017 16:00

                Вот это уже звучит как переход на личности

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

                значит до этого вы писали код, в который требовал больше времени и тяжело тестировался. Я сделал вывод что он был хуже, иначе зачем было переходить на Moya? Где тут личности?

                все преимущества я уже расписал.

                Вот это я прошу от вас — напишите преимущества, не в стиле «стало лучше, проще тестировать и быстрее» — это на рекламный проспект крема для кожи похоже (в смысле одна вода без конкретики).

                Давайте вы не будете указывать мне, где мне публиковать свои статьи.

                Вы протестуете против того, чтоб я указывал вам что делать и при этом указываете что делать мне. Не кажется вам это не последовательным?


          1. rule
            09.07.2017 14:32
            -1

            Ну и давайте ваше заключение разберем:

            В этой статье я хотел дать читателям базовое представление о мощной сетевой библиотеке Moya, .

            В чем вы измеряете мощность библиотек?

            намеренно опустив нюансы для того, чтобы вы самостоятельно смогли исследовать её и насладиться развитым инструментарием,

            Насколько я понял никакого инструментария она не предоставляет. Что вы имеете ввиду, API? Если так, то почему он развитый, как вы измерили/оценили его развитость?

            который позволяет решать широкий спектр задач при выстраивании сетевого слоя в iOS разработке.

            Какой это широкий спекрт задач, я хотел услышать хоть об одной из них. Ну и слова выстраивание вроде нет в русском языке.


            1. svyat_reshetnikov
              09.07.2017 15:35

              Такое ощущение, что Вам просто нечем заняться и вы решили придраться ко всему, к чему только можно, включая речевые обороты.

              Ну и слова выстраивание вроде нет в русском языке
              А вот это вообще самый последний аргумент. Я старался спокойно относиться к Вашим ошибкам, но Вы сами напросились. Разберём Ваш первый комментарий:
              Почитал очереднюю статью про Moya.
              И так не вижу вообще плюсов особых, вообще не убедительно. Ну и реактивность тут еще и пру строк кода добавила. Зачем она? Потому-что модно?
              Просто навязывает одно архитектурное решение.

              Ошибки: очереднюю(очередную), и так (в этом случае нужно писать слитно), нужна запятая после слова итак, пру (пару), потому-что (потому что).

              На этом считаю нашу дискуссию законченной.


              1. rule
                09.07.2017 15:55

                а на остальные вопросы не хотите ответить? Только граматику будем обсуждать?