RxSwift--让你的开发变得简单高效

作为一个RxSwift初学者,写这篇文章是很惶恐的(其中借鉴了很多大神的博文,后面会给出链接),但是管中窥豹,我会详解RxSwift官方Demo中的一段代码,让还没有接触RxSwift的小伙伴感受到RxSwift为什么会让我们的开发变得简单高效。

RxSwift到底是什么?

RxSwift是一种函数式响应式编程。那什么是函数式编程呢,函数式编程最重要的概念就是“无状态(immutable)”,看到这有些小伙伴可能会很开心,无状态(知名LOL职业选手)嘛,我是他的粉丝!言归正传,到底什么是“无状态(immutable)”呢?我看了很多文章,但是都被他们专业的描述整的一头雾水,我来说说我的看法:有丰富debug经验的小伙伴们都知道一个事实,程序中80%的BUG都可以由追踪变量的值来发现,为什么呢?反过来想,引起BUG的最大元凶正是值的改变。说道这里大家肯定都有了一个大胆的想法,倘若有一种方式能够消除状态的改变(这里为什么不说“值”,因为Rx不仅仅是值的“无状态(immutable)”,也会有事件的“无状态(immutable)”,所以准确的描述应该是“状态”),那么岂不是就没什么BUG了?函数式编程正是为此而生。

那函数式编程究竟是怎么做到“无状态(immutable)”这一点的呢?答案就是“序列(sequence)”,既然我不能改变状态,那我需要发生改变的时候,我就重新生成一个状态,发生几次改变,我就生成几个状态,而“序列(sequence)”就是盛放这些状态的容器(大家可以看做是一个数组),那么状态的改变是不是就不复存在?答案是肯定的,我只需要分别对“序列(sequence)”中的状态做出命令,他们之间就不会互相影响,那么出bug的几率自然大大降低。

那么既然成为了一个“序列”,怎么生成“序列”,怎么组合多个“序列”,怎么处理每个“序列”中的每个元素?RxSwift中有非常多的操作符来解决这些问题,这也是RxSwift难以学习的原因之一。但是我们只要理解,这些操作符的的最终目标是让“序列”变成我们想要的样子。记忆起来就会事半功倍。

说完函数式,再来说说响应式编程,先看段代码:

 var a: Int = 0
 var b: Int = 0
 var c = a + b
 a = 1
 print(c)

尽管a在c打印之前赋值为1,但是在给c赋值的时候 a + b 还是等于 0 ,我们要想让c得到最新的结果,就需要在c赋值之前将a的值改为1,那么如果a和b的值在之后的代码中会一直变动,就需要不断的给c写一行重新赋值的代码:c = a+ b,这真是太麻烦了,响应式就是为了解决这个麻烦诞生的,当a或者b发生变化的时候,c的值会响应a或者b的值发生变化,所以就被称之为响应式了,那么响应式如果实现呢,最简单容易理解的方式就是通过a或者b的属性监听器,当监听到a或者b发生变化时,就重新给c赋值。实际上,无论代码怎么写,无非是在某一层隐式调用了c = a + b这一行代码而已,即使我们直接去操作指针,也需要有一行代码将两个指针指向的值相加,所以响应式并不是难以理解的技术,但是响应式编程能够极大的改善代码的可读性,更直观,更容易维护。

RxSwift,就是将函数式,响应式,以及链式编程的结合体,即使在项目中由于各种原因不能去使用,读懂它,也能让你在写业务代码的时候更加游刃有余。

在这里再多说几句:有很多博客把“序列(sequence)”比作“流、信号、管道”等,这确实有助于大家理解RxSwift,但是RxSwift的项目维护者显然不这么认为,下面给大家看一段InfoQ对项目维护者Krunoslav Zaher的采访:

使用不一样的、不太准确的方式思考可观察的序列会带来很多问题(流、信号、导管等)。那些东西有时候可以用来帮助新手探索Rx,但是如果使用的时间过长,它们只会在接下来的开发中带来很多困惑。人们有时候使用它们是因为Rx可以用在模拟系统的多状态部分,但是那只是故事的一部分,而我建议大家不要那样思考。

使用那些项(流、信号、导管等)思考的问题在于它们带有一个隐含假定:
定义Observable时带有共享的值,并且可以从外部设定的当前值也是一样的。而Observable会被莫名其妙地直接取消,这看起来像future或promise。

而事实上:
Observable只是一个如何计算序列的定义。没有计算会在序列定义后(let result = source.map(transformObservableSequence))自动地执行。这和Swift中的SequenceSequence是一样的。当绑定一个观察者时(这与调用SequenceType中的generate方法相同)才会执行计算,并且可以通过处理Disposable对象的结果来取消特定的计算。Observable当然可以代表多状态的计算,它们共享潜在的绑定并source可观察的序列(使用shareReplay(1)操作等),但是这不是默认的行为。

我认为部分的问题在于也许人们一直在使用future、promise或其他更简单的、使用方式相似的reactive库,所以他们自然地认为Rx在其他的情况下也是同样的表现,而这对于新手来说显然是令人困惑的。
奇怪的是那些单方面的属性对刚开始使用Rx的人们造成了很大的问题,有时候门槛会变得太高以至于人们变的没有动力,但是从另一角度来看,这正是我个人认为Rx美的原因。

它美的地方在于你可以只通过一句话就能教会一个人使用Rx:Observable<T>代表了元素类型为T的可观察序列(与Swift中的SequenceSequence是一样的),其中每个元素都可以调用subscribe(和SequenceSequence中的generate方法相同)方法来注册自己的接受序列元素的observer(返回信号)。
如果这方面很清楚的话,所有的其他东西都只是细节,或变得非常明显和自然。
将Rx看作另一个库而不是一个不同的概念/抽象,并且开始在Stack
Overflow或相似的网站上开始查阅它,将会带来许多问题。在学习Rx时,唯一需要学习的部分就是理解可观察序列的句子,这样的话其他的东西就很显而易见了。这时候,陡峭的学习曲线就消失了。

RxSwift官方demo解析

下面是官网demo的一个小模块,实现了Github的注册功能,我们先看一下目录结构(没有标绿的文件是RxSwift专门为UI设计的更为精简的实现方式,这里就不多做探讨):

目录结构.png

显然这是一个标准的MVVM架构,在我看来RxSwift是MVVM的完美CP,RxSwift非常适合用来解决MVVM中,ViewModel的State和View绑定的问题。再扯一些题外话,demo中使用到了面向协议编程的思想,也是很值得大家参考的,能够大幅度提高代码的复用率和解耦合。推荐大家在慕课网上搜索2016swift开发者大会上,李洁信的演讲,在这里我就不附上地址了。

接着看疗效:


演示.gif

在正常情况下,完成2个密码的比对功能我们需要在textField的代理方法中监听text属性的变化,然后使用类似于这种结构的判断:

if  validPassWord(textfield.text) {
      tipLabel1.text = ...
      tipLabel.textColor = ...
} else {
      code
}

if textfield1.text == textfield2.text {
      tipLabel2.text = ...
      tipLabel2.textColor = ...
}else {
      code
}

事实上,实际情况远比我的伪代码复杂的多得多,大家应该都深有体会。那么RxSwift又是怎么完成这些功能的呢?我们先看一下viewController中的代码(为了方便大家理解我会添加注释):

override func viewDidLoad() {
        super.viewDidLoad()
        //  这里是viewModel的构造函数,构造函数会接收三个TextField中的text生成的“序列”
        //  只要textField.text发生改变,“序列”中就会有一个新的元素Element生成
        let viewModel = GithubSignupViewModel1(
            input: (
                // .rx.text.orEmpty.asObservable()是RxSwift提供的UIKit的扩展,可以快速生成一个可观察序列,以下同理   
                username: usernameOutlet.rx.text.orEmpty.asObservable(),
                password: passwordOutlet.rx.text.orEmpty.asObservable(),
                repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),
                loginTaps: signupOutlet.rx.tap.asObservable()
            ),
            dependency: (
                //  网络接口
                API: GitHubDefaultAPI.sharedAPI,
                //  验证输入内容是否合法的工具类
                validationService: GitHubDefaultValidationService.sharedValidationService,
                //  弹窗,点击登录按钮后提示信息
                wireframe: DefaultWireframe.shared
            )
        )

        //  viewModel构造函数中会处理出入的三个“序列”,然后生成以下五个可观察“序列”
        //  let validatedUsername: Observable<ValidationResult>          //  用户名是否可用
        //  let validatedPassword: Observable<ValidationResult>          //  密码是否可用
        //  let validatedPasswordRepeated: Observable<ValidationResult>  //  重复密码是否可用
        //  let signupEnabled: Observable<Bool>                          //  注册按钮是否可以点击
        //  let signedIn: Observable<Bool>                               //  是否玩成了注册(控制弹窗)
        //  let signingIn: Observable<Bool>                              //  是否在注册进程中(控制菊花的旋转)
       //   ValidationResult是一个枚举
       enum ValidationResult {
            case ok(message: String)
            case empty
            case validating
            case failed(message: String)
       }
        // 有了以上这些序列,就可以在控制器中完成数据和控件的绑定
        viewModel.signupEnabled
            //  订阅一个可观察序列的事件处理闭包,只要有新的元素添加到序列中,该方法就会自动发出信号并执行闭包中的代码,并返回一个遵守Disposable的类型,Disposable就相当于垃圾回收,会自动销毁序列。
            .subscribe(onNext: { [weak self] valid  in
                self?.signupOutlet.isEnabled = valid
                self?.signupOutlet.alpha = valid ? 1.0 : 0.5
            })
            .disposed(by: disposeBag)

        viewModel.validatedUsername
            /**
            bind其实跟subscribe是等价的,只是他能更好的反应可观察序列和观察者之间的关系
            站在一个代码阅读者的角度,用subscribe方法订阅,你可能需要在事件处理序列中去寻找谁是观察者,但是使用bind,你
            可以很直观的看到观察者是谁,其实就是相当于对subscribe再次封装一层,让观察者在自己的代码区域完成事
            件,就不用再写在控制器中,代码阅读起来真叫一个流畅,有一种一目了然的感觉。也让你不知不觉中就降低了
            代码的耦合度。
            */
            .bind(to: usernameValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)

        viewModel.validatedPassword
            .bind(to: passwordValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)

        viewModel.validatedPasswordRepeated
            .bind(to: repeatedPasswordValidationOutlet.rx.validationResult)
            .disposed(by: disposeBag)

        viewModel.signingIn
            .bind(to: signingUpOulet.rx.isAnimating)
            .disposed(by: disposeBag)

        viewModel.signedIn
            .subscribe(onNext: { signedIn in
                print("User signed in \(signedIn)")
            })
            .disposed(by: disposeBag)
        //}

        let tapBackground = UITapGestureRecognizer()
        tapBackground.rx.event
            .subscribe(onNext: { [weak self] _ in
                self?.view.endEditing(true)
            })
            .disposed(by: disposeBag)
        view.addGestureRecognizer(tapBackground)
    }

总结

本来很想详细的解构一下官方demo中的代码解构,真的是十分优秀简洁的代码,然后再说一说每个操作符的含义,但是这不是我写这篇文章的初衷,我只想让还没有开始接触到RxSwift的小伙伴稍稍感受一下RxSwift的魅力所在。如果你因为本文稍稍对RxSwift提起兴趣,并打算学学看,那么下面这些博文千万不要错过:
RxSwift函数响应式编程
RxSwift给Swift带来了原生Reactive编程的功能
是时候学习RxSwift了
[翻译]RxSwift入门

希望读者能指出本文中出现的错误,不胜感激。

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

推荐阅读更多精彩内容