Существует бесчисленное множество статей относительно шаблона 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<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)
Вот так! Теперь приложение получает данные о погоде, в то время как пользователь вводит название города, и неважно, что пользователь видит, истинное состояние приложения остается скрытым.
Если Вы заинтересовались расширенной версией этого приложения, посмотрите мой пример приложения о погоде на Github.
ссылка на оригинал статьи http://habrahabr.ru/post/273455/
Добавить комментарий