RxSwift by Examples #2 – Observable and the Bind

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))()
        }
}

分步讲解:

  1. 转换变量成 Observable - 因为 Variable 可以是 Observer 也可以是 Observable,所以我们要决定它是哪一个。又因为我们想观察它,于是把它转换成 Observable。
  2. Map 每个新的 CGPoint 到 UIColor。我们会得到 Observable 产生的新的中心位置,经过计算,得到新的 UIColor。
  3. 你可能注意到 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。

你可能感兴趣的:(RxSwift by Examples #2 – Observable and the Bind)