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

分步讲解:

  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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容