Это мой перевод второй части из цикла статей Лукаша Мроза (Łukasz Mróz), где рассказывает в примерах о том, как использовать RxSwift. В ней даётся несколько важных определений из мира Rx, знание которых пригодится для лучшего понимания последующих материалов и самой темы.
В первой части “RxSwift в примерах” мы изучили основы RxSwift и RxCocoa (если вы её ещё не читали, я очень рекомендую это сделать!). Пришло время расширить наши знания о “реактивном” пути. Сегодня мы поговорим о привязках (bindings).
Не волнуйтесь, привязка (binding) всего лишь означает, что мы связываем наблюдаемое (observables) с субъектом (subjects). Но сначала мы должны познакомится с дополнительной терминологией.
Определения
До того, как мы начнём, нам нужно затронуть некоторые определения. Мы уже узнали о наблюдаемом и наблюдателях, и сегодня мы рассмотрим другие типы.
Субъект (Subject) – одновременно наблюдаемое и наблюдатель. Он может как наблюдать, так и наблюдаться.
Поведенческий субъект (BehaviourSubject) – подписываясь на него, вы получите последнее значение, отправленное субъектом, и далее значения, отправленные после подписки.
Публикующий субъект (PublishSubject) – подписываясь на него, вы будете получать только значения, отправленные после подписки.
Повторяющий субъект (ReplaySubject) – подписываясь на него, вы будете получать не только значения, которые были отправлены после подписки, но и значение, посланные до неё. Как много старых значение вы получите? Это зависит от размера буфера повторяющего субъекта, на который вы подписались. Вы указываете это при инициализации субъекта.
Для упрощения, приведём пример. У вас идёт празднование дня рождения, и вы открываете полученные подарки. Вы открыли первый, второй, третий. И оопс! Ваша мама готовила что-то очень вкусное и опоздала к началу вечеринки. Как мать, она просто должна знать, какие подарки вы уже получили. Поэтому вы говорите ей о них. В мире Rx вы отсылаете подарки — наблюдаемую последовательность (observable sequence) – своей матери — наблюдателю (observer). Что интересно, она начала наблюдать за вами после того, как вы уже произвели несколько значений, но она всё равно получила всю информацию. Для неё вы были повторяющий субъект (ReplaySubject) с буфером = 3 (вы сохраняете три последних подарка и сообщаете о них каждый раз, когда появляется новый подписчик).
Вы всё ещё открываете подарки и вдруг видите, что два ваших друга (Джэк и Энди) тоже опоздали на вечеринку. Джэк ваш хороший друг, поэтому он спрашивает, что вы уже открыли. Поскольку вы немного злы, что он пропустил часть вечеринки, вы говорите ему только о последнем открытом подарке. Он не знает, что их было больше, поэтому ему этого достаточно. В мире Rx вы послали только последнее произведённое значение наблюдателю (Джэку). Он также получит последующие значения, как только вы произведёте их (открыв следующий подарок). Для него вы поведенческий субъект (BehaviourSubject).
Но есть ещё Энди, который другом, который не особо переживает об уже открытых подарках, поэтому он просто сидит и ждёт оставшейся части представления. Как вы понимаете, для него вы всего лишь публикующий субъект (PublishSubject). Он получает лишь значения, произведённые после подписки.
Ещё существует нечто, называемое переменной (Variable). Это обёртка вокруг поведенческого субъекта (BehaviourSubject). Суть в том, что вы можете только оправить следующее .onNext() событие (тогда как используя поведенческий субъект, у вас есть прямой доступ к отправке ошибки .onError() и завершения .onCompleted()). Также, переменная (Variable) автоматически отправляет событие завершения .onCompleted(), когда освобождает занимаемую память.
Ну что ж, достаточно определений. Давайте практиковаться!
Пример
Мы создадим простое приложение, которое свяжет цвет мяча с его позицией в представлении (view), и цвет фона с цветом мяча.
Сначала давайте создадим проект также, как мы сделали это в предыдущем туториале. Мы также будем использовать CocoaPods, но в дополнение к RxSwift и RxCocoa вы используем Chameleon, чтобы удобно связать цвета. Наш Podfile должен выглядеть так:
1 2 3 4 5 6 7 8 9 10 |
platform :ios, '8.0' use_frameworks! target 'ColourfulBall' do pod 'RxSwift' pod 'RxCocoa' pod 'ChameleonFramework/Swift' end |
После настройки проекта мы можем начать кодить! Сначала мы нарисуем окружность в основном представлении (view) нашего контроллера. Мы сделаем это из кода, но если вы хотите сделать это в Interface Builder – пожалуйста. Пример создания этого представления выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import ChameleonFramework import UIKit import RxSwift import RxCocoa class ViewController: UIViewController { var circleView: UIView! override func viewDidLoad() { super.viewDidLoad() setup() } func setup() { // Добавляем представление с окружностью circleView circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0))) circleView.layer.cornerRadius = circleView.frame.width / 2.0 circleView.center = view.center circleView.backgroundColor = UIColor.greenColor() view.addSubview(circleView) } } |
Этот код должен быть понятным (мы просто создали скруглённый UIView), так что двигаемся дальше. Следующим шагом будет перемещение нашего мяча жестами. Чтобы это сделать, нужно добавить UIPanGestureRecognizer и изменить его местоположение (frame).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func setup() { // Добавляем представление с окружностью circleView circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0))) circleView.layer.cornerRadius = circleView.frame.width / 2.0 circleView.center = view.center circleView.backgroundColor = UIColor.greenColor() view.addSubview(circleView) // Добавляем распознователь жестов let gestureRecognizer = UIPanGestureRecognizer(target: self, action: "circleMoved:") circleView.addGestureRecognizer(gestureRecognizer) } func circleMoved(recognizer: UIPanGestureRecognizer) { let location = recognizer.locationInView(view) UIView.animateWithDuration(0.1) { self.circleView.center = location } } |
Отлично! Теперь наше приложение должно выглядеть приблизительно так:
Следующим шагом будет связка с чего-то! Давайте соединим позицию мяча с его цветом. Как это сделать? Сначала, мы будем наблюдать (observe) позицию центра мяча, используя rx_observe(), а затем привяжем (bind) его к переменной (variable), используя bindTo(). Но что делает связка в нашем случае? Каждый раз, когда наш мяч испускает новую позицию, переменная получит об этом новый сигнал. В этом случае наша переменная является наблюдателем, потому что она наблюдает позицию.
Мы создадим эту переменную в модели представления ViewModel, которая будет использоваться для расчётов, связанных с пользовательским интерфейсом. Тогда каждый раз, когда наша переменная получит новую позицию, мы будем рассчитывать новый цвет фона для мяча. Просто, правда?
Теперь нам нужно создать нашу ViewModel. Это будет легко, потому что нам понадобятся только два свойства: centerVariable, которая будет нашим наблюдателем и наблюдаемым – мы будем и сохранять туда данные и получать. Вторая будет backgroundColorObservable. Вообще говоря, это не переменная, а только наблюдаемое.
Мы можете спросить: “Почему centerVariable – переменная, а backgroundColorObservable – наблюдаемое?” Отличный вопрос! Смотрите, наш наблюдаемый центр мяча соединён с centerVariable. Это означает, что когда центр изменится, centerVariable получит изменения. Тогда это наблюдатель. Также, в нашей ViewModel мы использовали centerVariable как наблюдателя, что делает её одновременно и наблюдателем и наблюдаемым, что называется субъектом. Почему переменная, а не публикующий субъект или повторяющий субъект? Потому что мы хотим быть уверенными, что мы получим последние координаты центра мяча каждый раз, когда мы подписываемся на него.
backgroundColorObservable – всего лишь наблюдаемое, оно никогда не связывается ни с чем, что делает логичным оставить его всего лишь наблюдаемым.
Отлично! Достаточно теории, давайте кодить! Наша базовая ViewModel Должна выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import ChameleonFramework import Foundation import RxSwift import RxCocoa class CircleViewModel { var centerVariable = Variable(.zero) // Создаём переменную, которая будет изменена и наблюдаема var backgroundColorObservable: Observable! // Создаём наблюдаемое, которое будет изменять фоновый цвет backgroundColor в зависимости от координат центра init() { setup() } setup() { } } |
Отлично. Теперь нам нужно настроить наше backgroundColorObservable. Мы хотим его изменять, основываясь на новом CGPoint, прозведённым centerVariable.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func setup() { // Когда мы получаем новые координаты центра, производим новый UIColor backgroundColorObservable = centerVariable.asObservable() .map { center in guard let center = center else { return UIColor.flatten(.black)() } let red: CGFloat = ((center.x + center.y) % 255.0) / 255.0 // Мы просто манипулируем красным, но вы можете делать то же самое с белым или зелёным let green: CGFloat = 0.0 let blue: CGFloat = 0.0 return UIColor.flatten(UIColor(red: red, green: green, blue: blue, alpha: 1.0))() } } |
Шаг за шагом:
- Трансформируем нашу переменную в наблюдаемое – поскольку переменная может быть как наблюдателем, так и наблюдаемым, на нужно решить, кем она будет. Т.к. мы хотим наблюдать за ней, мы преобразуем её в наблюдаемое.
- Отображаем каждое новое значение CGPoint в UIColor. Мы получаем новые координаты центра, произведённые нашим наблюдаемым. Основываясь на очень (или не очень) сложных математических расчётах мы создаём новый UIColor.
- Мы могли заметить, что наше наблюдаемое является опциональным (optional) CGPoint. Почему? Мы объясним это через секунду. Но нам нужно защитить себя и, в случае получения nil, вернуть какой-нибудь цвет по-умолчанию (чёрный, в нашем случае).
Хорошо. Мы уже близки к концу. Теперь у нас есть наблюдаемое, которое будет производить новый цвет фона для нашего мяча. Нам только нужно обновлять наш мяч на основе новых значений. Теперь это действительно просто. Это похоже на первую часть этой серии. Мы подпишемся subscribe() на наблюдаемое.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Подписываемся на backgroundObservable чтобы получить новый цвета от ViewModel. circleViewModel.backgroundColorObservable .subscribe(onNext: { [weak self] backgroundColor in UIView.animateWithDuration(0.1) { self?.circleView.backgroundColor = backgroundColor // Пробуем получить дополнительный цвет для данного фонового let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor) // Если он отличается от него if viewBackgroundColor != backgroundColor { // Назначим его фоновым для view // Нам всего лишь нужен другой цвет, чтобы разглядеть окружность в представлении view self?.view.backgroundColor = viewBackgroundColor } } }) .addDisposableTo(disposeBag) |
Как вы можете заметить, мы также добавили изменение фонового цвета нашего представления (view) на дополнительный цвет нашего мяча. Также мы проверили, если дополнительный цвет совпадает с цветом мяча (мы же хотим его хотя бы видеть!). Но это фича, а не основная задача. Вам нужно добавить этот код в метод setup(), чтобы это выглядело похожим на следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
func setup() { // Добавляем представление окружности circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0))) circleView.layer.cornerRadius = circleView.frame.width / 2.0 circleView.center = view.center circleView.backgroundColor = .green view.addSubview(circleView) circleViewModel = CircleViewModel() // Связываем центральную точку CircleView с centerObservable circleView .rx.observe(CGPoint.self, "center") .bindTo(circleViewModel.centerVariable) .addDisposableTo(disposeBag) // Подписываемся на backgroundObservable, чтобы получать новые цвета от ViewModel. circleViewModel.backgroundColorObservable .subscribe(onNext: { [weak self] backgroundColor in UIView.animateWithDuration(0.1) { self?.circleView.backgroundColor = backgroundColor // Пробуем получить дополнительный цвет для данного фонового let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor) // Если он отличается от него if viewBackgroundColor != backgroundColor { // Назначим его фоновым для view // Мам всего лишь нужен другой цвет, чтобы разглядеть окружность в представлении self?.view.backgroundColor = viewBackgroundColor } } }) .addDisposableTo(disposeBag) let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(circleMoved(_:))) circleView.addGestureRecognizer(gestureRecognizer) } |
И мы закончили! Вся задача манипулирования цветами решена без делегатов, сообщений и всего того шаблонного кода, который мы обычно используем для такого типа задач. Результат должен быть похож на картинку, показанную вначале Примера.
Теперь вы можете попробовать модифицировать его! Возможно, связать центр с размером мяча? Тогда попробуйте изменить радиус углов cornerRadius на основе его ширины width и высоты height? Это на самом деле ваше дело, но я думаю, что с Rx эти задачи действительно решаются восхитительно.
Это всё на сегодня и, как всегда, весь проект доступен на GitHub.
Оригинал: RxSwift by Examples#2 – Observable and the Bind.
Полезная статья? Их будет больше, если вы поддержите меня!