RxSwift by Examples 分成 4 部分。以下是 PART 2 的学习笔记和翻译整理。原文在这里。
binding 意思是连接 Observable 和 Subject。
释义
我们已经学习过 Observable 和 Observer。
- Subject - 可观察的和观察者。它可以观察和被观察。
- BehaviorSubject - 当你订阅它,你将得到它已发出的最新的值,以及此后发出的值。
- PublishSubject - 当你订阅它,你只能得到此后它发出的值。
- ReplaySubject - 当你订阅它,你将得到此后发出的值,但也能得到此前发出的值。可以得到多早以前发出的值呢?这取决于你所订阅的 ReplaySubject 的缓存大小(buffer size)。
举例说:你正在举行生日派对,你要打开你收到的礼物。
你打开了第一个、第二个、第三个礼物。你妈妈正在厨房里准备美味的食物,因此还没来到派对现场。作为一个妈妈,她想知道你得到了什么礼物。于是你将情况告诉她。在 Rx 的世界中,你发送可观察的序列 obserbable sequence(礼物)给观察者 observer(你妈妈)。有意思的是,她在你已经发出了若干值之后开始观察,但是不管怎样她得到了完整的信息。对她我们是一个 buffer = 3 的 ReplaySubject(我们保留 3 个最新的礼物发送给每一个新的订阅者)。
你继续打开礼物。这时你看到你的两个朋友 Jack 和 Andy 也来到派对。Jack 是你的好朋友,所以他问你目前打开了些什么。你对他的迟到有些生气,所以你只告诉他你最后一次打开的礼物。他并不知道还有其他礼物,所以他很高兴。在 Rx 的世界中,你只发送了最近的一个值给观察者(Jack)。他还将得到接下来的值当你发送的时候(接下来你将打开的礼物)。对他而言我们是一个 BehaviorSubject。
还有一个 Andy,他只是一个普通朋友,并不真的在意你已经打开了什么礼物。所以他只是坐下来等待接下来的表演。如你所料,对他而言我们只是一个 PublishSubject。他只得到在他订阅之后发出的值。
还有一个概念叫 Variable。这是一个对 BehaviorSubject 的包装。你只能提交 .onNext() 事件(当使用 BehaviorSubject 的时候你可以直接发送 .onError(), .onCompleted())。Variable 自动发送 .onCompleted() 事件,当它被注销的时候。
示例
我们将创建一个简单的 app,在视图中连接球的颜色与位置,我们还将连接视图背景色与球体的颜色。
我们创建项目,并使用 Cocoapods 引入 RxSwift 和 RxCocoa,我们还将使用 Chameleon 来连接颜色。
Podfile
platform :ios, '9.0'
use_frameworks!
target 'ColourfulBall' do
pod 'RxSwift'
pod 'RxCocoa'
pod 'ChameleonFramework/Swift', :git => 'https://github.com/ViccAlexander/Chameleon.git'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_TESTABILITY'] = 'YES'
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end
在我们的 Controller 的 main view 中画一个圆形。
import ChameleonFramework
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
var circleView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
// Add circle view
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)
}
}
下一步,添加 UIPanGestureRecognizer 并根据手势改变球形的 frame
func setup() {
// Add circle view
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)
// Add gesture recognizer
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(circleMoved(_:)))
circleView.addGestureRecognizer(gestureRecognizer)
}
func circleMoved(_ recognizer: UIPanGestureRecognizer) {
let location = recognizer.location(in: view)
UIView.animate(withDuration: 0.1) {
self.circleView.center = location
}
}
Bind
下一步我们将做绑定。连接球体位置与球体颜色。怎样做呢?
首先我们将用 rx.observe() 观察球体的中心位置,然后用 bindTo() 绑定给一个变量 Variable。在我们的例子中,绑定做了什么事情呢?每一次一个我们的球体发送新的位置信息,变量将收到关于它的新的信号。我们的变量是一个观察者,因为它观察位置。
我们将在一个 ViewModel 中创建变量,我们用它来计算 UI 的东西。在这个例子中,每次变量得到一个新的地址,我们将为球体计算新的背景色。
我们只有两个属性:centerVariable 将是我们的 observer 和 observable - 我们保存数据给它,并从它获取数据。另一个是 backgroundColorObserable。它实际上不是一个变量,只是一个 obserable。
你可能会问为什么 centerVariable 是一个 Variable 而 backgroundColorObserable 是一个 Obserbable?
我们球体的 observable center 连接了 centerVariable,这意味着任何时候 center 改变,centerVAriable 将得到这个改变。因此这是一个观察者 Observer。同时我们 ViewModel 中的 centerVariable 作为一个 Obserable,同时作为 observer 和 Observable 就是一个 Subject。
为什么是 Variable 而不是 PublishSubject 或者 ReplaySubject?因为我们想确保我们将得到订阅时的最新的球体中心值。
backgroundColorObservable 只是一个 Observable,它从未被任何东西所约束,所以它只是一个可观察的 Observable。
ViewModel
我们的基础 ViewModel 是这样
import ChameleonFramework
import Foundation
import RxSwift
import RxCocoa
class CircleViewModel {
var centerVariable = Variable(.zero) // Create one variable that will be changed and observed
var backgroundColorObservable: Observable! // Create observable that will change backgroundColor based on center
init() {
setup()
}
func setup() {
}
}
接着我们需要设置 backgroundColorObserable。我们希望它基于由 centerVariable 产生的新的 CGPoint 而改变。
func setup() {
// When we get new center, emit new UIColor
backgroundColorObservable = centerVariable.asObservable()
.map { center in
guard let center = center else { return UIColor.flatten(.black)() }
let red: CGFloat = (center.x + center.y).truncatingRemainder(dividingBy: 255.0) / 255.0 // We just manipulate red, but you can do w/e
let green: CGFloat = 0.0
let blue: CGFloat = 0.0
return UIColor.flatten(UIColor(red: red, green: green, blue: blue, alpha: 1.0))()
}
}
分步讲解:
- 转换变量成 Observable - 因为 Variable 可以是 Observer 也可以是 Observable,所以我们要决定它是哪一个。又因为我们想观察它,于是把它转换成 Observable。
- Map 每个新的 CGPoint 到 UIColor。我们会得到 Observable 产生的新的中心位置,经过计算,得到新的 UIColor。
- 你可能注意到 Observable 是一个 optional 的 CGPoint。为什么?我们需要在得到 nil 的时候保护自己,返回默认值。
现在我们有了 Observable 的背景色。我们需要基于新的值更新球体。这非常简单,我们将 subscribe() 这个 Observable。
fileprivate var circleViewModel: CircleViewModel!
fileprivate let disposeBag = DisposeBag()
然后
circleViewModel = CircleViewModel()
// Subscribe to backgroundObservable to get new colors from the ViewModel.
circleViewModel.backgroundColorObservable
.subscribe(onNext: { [weak self] backgroundColor in
UIView.animateWithDuration(0.1) {
self?.circleView.backgroundColor = backgroundColor
// Try to get complementary color for given background color
let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor)
// If it is different that the color
if viewBackgroundColor != backgroundColor {
// Assign it as a background color of the view
// We only want different color to be able to see that circle in a view
self?.view.backgroundColor = viewBackgroundColor
}
}
})
.addDisposableTo(disposeBag)
我们同时还把视图背景色变成球体颜色的补色。同时检查这个补色是否和球体颜色一样(确保我们看得到球体)。我们可以把这段代码放在 setup() 中
func setup() {
// Add circle view
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()
// Bind the center point of the CircleView to the centerObservable
circleView
.rx.observe(CGPoint.self, "center")
.bindTo(circleViewModel.centerVariable)
.addDisposableTo(disposeBag)
// Subscribe to backgroundObservable to get new colors from the ViewModel.
circleViewModel.backgroundColorObservable
.subscribe(onNext: { [weak self] backgroundColor in
UIView.animateWithDuration(0.1) {
self?.circleView.backgroundColor = backgroundColor
// Try to get complementary color for given background color
let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor)
// If it is different that the color
if viewBackgroundColor != backgroundColor {
// Assign it as a background color of the view
// We only want different color to be able to see that circle in a view
self?.view.backgroundColor = viewBackgroundColor
}
}
})
.addDisposableTo(disposeBag)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(circleMoved(_:)))
circleView.addGestureRecognizer(gestureRecognizer)
}
完成。整个操纵颜色的任务没有用到做类似事情时我们通常要用到的 delegate, notification。
也许你可以试着绑定中心位置和球体尺寸,试着基于 width 和 height 改变 cornerRadius。