Реализация MVVM в iOS с помощью RxSwift

в 14:49, , рубрики: Cocoa, ios development, reactive programming, RxSwift, swift, разработка мобильных приложений, разработка под iOS

Существует бесчисленное множество статей относительно шаблона MVVM в iOS, но немного о RxSwift, и мало кто акцентирует внимание на том, как выглядит паттерн MVVM на практике и как его реализовать.

ReactiveX

ReactiveX — библиотека для создания асинхронных и основанных на событии программ при помощи наблюдаемой последовательности.  — reactivex.io

RxSwift — относительно молодой фреймворк, который позволяет "реактивно программировать". Если Вы ничего о нем не знаете, тогда наведите справки, потому что функциональное реактивное программирование (FRP) набирает обороты, и не собирается останавливаться.

Итак, как же выглядит MVVM в iOS?

Способ соединения модели с ViewController при использований шаблона MVC часто выглядит, как некий хак. Вы, как правило, вызываете что-то вроде функции updateUI() в контроллере, когда думаете, что модель изменилась Это может привести к появлению несоответствий между моделью и ViewController, ненужным обновлениям и непонятным ошибкам.

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

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

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

Однако, ViewController отображает на экране не только данные, он также получает ввод данных от пользователя. Так как наш ViewController — просто прокси-сервер, он не может использовать эти данные, поэтому все, что он должен сделать, это передать их в ViewModel, и ViewModel сделает все остальное.

image

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

Все это звучит прекрасно, но как мы это сделаем?

MVVM в связке с RxSwift

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

Эта статья предполагает, что вы знакомы с RxSwift. Если Вы ничего не знаете о нем, то смело читайте дальше, но я предлагаю больше узнать о ReactiveX.

post_images

У нас есть UITextField для ввода названия города и парочька UILabels, чтобы вывести на экран текущую температуру.

Примечание: для этого приложения я буду получать данные о погоде из OpenWeatherMap.

Так, наша модель будет классом Weather со несколькими свойствами name и degrees и инициализатором, который принимает объект JSON, который он анализирует и устанавливает свойства.

class Weather {
   var name:String?
   var degrees:Double?
 
   init(json: AnyObject) {
      let data = JSON(json)
      self.name = data["name"].stringValue
      self.degrees = data["main"]["temp"].doubleValue
   }
}

Примечание: SwiftyJSON является обязательным для разбора JSON в Swift.

Теперь мы должны позволить ViewModel управлять моделью посредством общедоступного (public) свойства searchText, к которому у ViewController позже будет доступ.

class ViewModel {
 
  private struct Constants {
      static let URLPrefix = "http://api.openweathermap.org/data/2.5/weather?q="
      static let URLPostfix = "/* my openweathermap APPID */"
    }
 
   let disposeBag = DisposeBag()
 
   var searchText = PublishSubject<String?>()

Наш searchText является объектом PublishSubject. Subject’ы одновременно являются и Observable и Observer. Другими словами, вы можете отправить им элементы, которые они могут повторно сгенерировать.

PublishSubjects уникальны, потому что когда данные передаются в PublishSubject, он рассылает их всем подписчикам, которые подписаны на него в данный момент. Нам нужно это, потому что в MVVM, в зависимости от жизненного цикла приложения, Observable в различных классах иногда могут получать элементы, прежде чем вы подпишетесь на них. Как только ViewController подписывается на свойство ViewModel, он должен увидеть, что с последним элементом все в порядке, чтобы вывести его на экран, и наоборот.

Теперь мы должны объявить свойство в ViewModel для каждого элемента UI, который вы хотите изменить программно.

var cityName = PublishSubject<String>()
var degrees = PublishSubject<String>()

Давайте также установим свойство для нашей модели и изменим свойства каждый раз, когда наша модель изменяется. Мы сделаем это путем объединения ‘старомодного’ способа (Наблюдатели свойства Swift) с помощью Rx. Мы отправим свойства объекта Weather в наш PublishSubjects, таким образом, они смогут сгенерировать значения в модели.

var weather:Weather? {
   didSet {
      if let name = weather?.name {
         dispatch_async(dispatch_get_main_queue()) {
            self.cityName.onNext(name)
         }
      }
      if let temp = weather?.degrees {
         dispatch_async(dispatch_get_main_queue()) {
            self.degrees.onNext("(temp)°F")
         }
      }
   }
}

Примечание: Нам нужно убедиться, что это выполняется в основном потоке, так как метод onNext() выполняется в другом потоке! (метод onNext отправляет элемент Observer).

Теперь давайте присоединим нашу модель к свойству searchText, которое мы объявили выше. Мы сделаем это путем создания NSURLRequest каждый раз, когда в searchText вносятся изменения, и затем подпишем нашу модель на тот запрос. Мы сделаем это в методе init(), потому что мы знаем, что все наши свойства устанавливаются, когда вызывается метод init().

init() {
   let jsonRequest = searchText
      .map { text in
         return NSURLSession.sharedSession().rx_JSON(self.getURLForString(text)!)
      }
      .switchLatest()
 
   jsonRequest
      .subscribeNext { json in
         self.weather = Weather(json: json)
       }
      .addDisposableTo(disposeBag)
}

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

Примечание: Метод rx_JSON() является фактически наблюдаемой последовательностью. Таким образом, jsonRequest — фактически Observable из Observable. Вот почему мы используем .switchLatest(), который заботиться о том, что jsonRequest возвращает только новую последовательность. Также имейте в виду, что запрос не начнет выборку, пока Вы не подпишитесь на него.

Теперь осталось только соеденить ViewController с ViewModel. Мы сделаем это за счет привязки PublishSubjects в ViewModel к аутлету в Controller.

class ViewController: UIViewController {
 
   let viewModel = ViewModel()
   let disposeBag = DisposeBag()
 
 
   @IBOutlet weak var nameTextField: UITextField!
 
   @IBOutlet weak var degreesLabel: UILabel!
   @IBOutlet weak var cityNameLabel: UILabel!
   override func viewDidLoad() {
      super.viewDidLoad()
 
      //Binding the UI
      viewModel.cityName.bindTo(cityNameLabel.rx_text)
         .addDisposableTo(disposeBag)
 
      viewModel.degrees.bindTo(degreesLabel.rx_text)
         .addDisposableTo(disposeBag)
   }
}

Не забывайте, что мы также должны убедиться, что наш ViewModel знает то, что пользователь ввел в текстовом поле! Мы можем сделать это путем отправки значения nameTextField, каждый раз, когда оно изменяется, к searchText свойству ViewModel. Так что, мы просто добавим это в методе viewDidLoad ():

nameTextField.rx_text.subscribeNext { text in
   self.viewModel.searchText.onNext(text)
   }
   .addDisposableTo(disposeBag)

image

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

Если Вы заинтересовались расширенной версией этого приложения, посмотрите мой пример приложения о погоде на Github.

Автор: yarmolchuk

Источник


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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js