[Перевод] Реализация MVVM в iOS с помощью RxSwift
Существует бесчисленное множество статей относительно шаблона MVVM в iOS, но немного о RxSwift, и мало кто акцентирует внимание на том, как выглядит паттерн MVVM на практике и как его реализовать.
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 сделает все остальное.
Это, в некотором смысле, односторонняя связь между ViewController и ViewModel. ViewController может видеть и обращаться к ViewModel, но у ViewModel нет ни малейшего представления о том, кто такой ViewController. Это значит, что Вы можете полностью удалить ViewController из своего приложения, и вся ваша логика будет работать так, как задумано!
Все это звучит прекрасно, но как мы это сделаем?
MVVM в связке с RxSwift
Давайте сделаем простое приложение, которое отображает прогноз погоды в городе, который вводит пользователь.
Эта статья предполагает, что вы знакомы с RxSwift. Если Вы ничего не знаете о нем, то смело читайте дальше, но я предлагаю больше узнать о ReactiveX.
У нас есть 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()
Наш searchText является объектом PublishSubject. Subject«ы одновременно являются и Observable и Observer. Другими словами, вы можете отправить им элементы, которые они могут повторно сгенерировать.
PublishSubjects уникальны, потому что когда данные передаются в PublishSubject, он рассылает их всем подписчикам, которые подписаны на него в данный момент. Нам нужно это, потому что в MVVM, в зависимости от жизненного цикла приложения, Observable в различных классах иногда могут получать элементы, прежде чем вы подпишетесь на них. Как только ViewController подписывается на свойство ViewModel, он должен увидеть, что с последним элементом все в порядке, чтобы вывести его на экран, и наоборот.
Теперь мы должны объявить свойство в ViewModel для каждого элемента UI, который вы хотите изменить программно.
var cityName = PublishSubject()
var degrees = PublishSubject()
Давайте также установим свойство для нашей модели и изменим свойства каждый раз, когда наша модель изменяется. Мы сделаем это путем объединения «старомодного» способа (Наблюдатели свойства 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)
Вот так! Теперь приложение получает данные о погоде, в то время как пользователь вводит название города, и неважно, что пользователь видит, истинное состояние приложения остается скрытым.
Если Вы заинтересовались расширенной версией этого приложения, посмотрите мой пример приложения о погоде на Github.