RxSwift by Examples 分成 4 部分。以下是 PART 2 的學習筆記和翻譯整理。原文在這里。
binding 意思是連接 Observable 和 Subject。
釋義
我們已經學習過 Observable 和 Observer。
- Subject - 可觀察的和觀察者。它可以觀察和被觀察。
- BehaviorSubject - 當你訂閱它,你將得到它已發(fā)出的最新的值,以及此后發(fā)出的值。
- PublishSubject - 當你訂閱它,你只能得到此后它發(fā)出的值。
- ReplaySubject - 當你訂閱它,你將得到此后發(fā)出的值,但也能得到此前發(fā)出的值??梢缘玫蕉嘣缫郧鞍l(fā)出的值呢?這取決于你所訂閱的 ReplaySubject 的緩存大?。╞uffer size)。
舉例說:你正在舉行生日派對,你要打開你收到的禮物。
你打開了第一個、第二個、第三個禮物。你媽媽正在廚房里準備美味的食物,因此還沒來到派對現(xiàn)場。作為一個媽媽,她想知道你得到了什么禮物。于是你將情況告訴她。在 Rx 的世界中,你發(fā)送可觀察的序列 obserbable sequence(禮物)給觀察者 observer(你媽媽)。有意思的是,她在你已經發(fā)出了若干值之后開始觀察,但是不管怎樣她得到了完整的信息。對她我們是一個 buffer = 3 的 ReplaySubject(我們保留 3 個最新的禮物發(fā)送給每一個新的訂閱者)。
你繼續(xù)打開禮物。這時你看到你的兩個朋友 Jack 和 Andy 也來到派對。Jack 是你的好朋友,所以他問你目前打開了些什么。你對他的遲到有些生氣,所以你只告訴他你最后一次打開的禮物。他并不知道還有其他禮物,所以他很高興。在 Rx 的世界中,你只發(fā)送了最近的一個值給觀察者(Jack)。他還將得到接下來的值當你發(fā)送的時候(接下來你將打開的禮物)。對他而言我們是一個 BehaviorSubject。
還有一個 Andy,他只是一個普通朋友,并不真的在意你已經打開了什么禮物。所以他只是坐下來等待接下來的表演。如你所料,對他而言我們只是一個 PublishSubject。他只得到在他訂閱之后發(fā)出的值。
還有一個概念叫 Variable。這是一個對 BehaviorSubject 的包裝。你只能提交 .onNext() 事件(當使用 BehaviorSubject 的時候你可以直接發(fā)送 .onError(), .onCompleted())。Variable 自動發(fā)送 .onCompleted() 事件,當它被注銷的時候。
示例
我們將創(chuàng)建一個簡單的 app,在視圖中連接球的顏色與位置,我們還將連接視圖背景色與球體的顏色。
我們創(chuàng)建項目,并使用 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。在我們的例子中,綁定做了什么事情呢?每一次一個我們的球體發(fā)送新的位置信息,變量將收到關于它的新的信號。我們的變量是一個觀察者,因為它觀察位置。
我們將在一個 ViewModel 中創(chuàng)建變量,我們用它來計算 UI 的東西。在這個例子中,每次變量得到一個新的地址,我們將為球體計算新的背景色。
我們只有兩個屬性:centerVariable 將是我們的 observer 和 observable - 我們保存數(shù)據給它,并從它獲取數(shù)據。另一個是 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<CGPoint?>(.zero) // Create one variable that will be changed and observed
var backgroundColorObservable: Observable<UIColor>! // 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 的時候保護自己,返回默認值。
現(xiàn)在我們有了 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。