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

от автора

Существует бесчисленное множество статей относительно шаблона 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.

ссылка на оригинал статьи http://habrahabr.ru/post/273455/