FrizzleFur / DailyLearning

My Daily Learning~
MIT License
60 stars 23 forks source link

Swift进阶 #33

Open FrizzleFur opened 5 years ago

FrizzleFur commented 5 years ago

Swift进阶

Guide

ipader/SwiftGuide: Swift Featured Projects in brain Mapping

Swift函数

Swift 3关于函数类型的一项重要提议 - 泊学 - 一个全栈工程师的自学网站

Alamofire

基于Alamofire的HTTP通信 - 泊学 - 一个全栈工程师的自学网站

RxSwift

MVVM

Master MVVM in Swift - 泊学 - 一个全栈工程师的自学网站

算法

Algorithms in Swift 3 - 泊学 - 一个全栈工程师的自学网站

FrizzleFur commented 5 years ago

RxSwift

为什么要使用 RxSwift ?

To create responsive and robust applications, you have to handle a multitude of concurrent tasks like playing audio, handling user interface input, making networking calls, and more. Sometimes, passing data from one process to another or even just observing that tasks happen in the correct sequence one after another asynchronously might cause the developer a lot of trouble you’ll learn how RxSwift solves the issues related to asynchronous programming and master various reactive techniques, from observing simple data sequences, to combining and transforming asynchronous value streams, to designing the architecture and building production quality apps.

异步编程

You’ve likely realized that some of the core issues with writing asynchronous code are: a) the order in which pieces of work are performed b) shared mutable data.

RxSwift是?

RxSwift本质上是通过允许代码对新数据做出反应并以连续,孤立的方式处理它来简化异步程序的开发。

RxSwift is a library for composing asynchronous and event-based code by using observable sequences and functional style operators, allowing for parameterized execution via schedulers.

我们先看一下 RxSwift 能够帮助我们做些什么:

Target Action、闭包回调、代理、通知、KVO

你不需要去管理观察者的生命周期,这样你就有更多精力去关注业务逻辑。

这样实现的代码更清晰,更简洁并且更准确。

例子

  1. 多个任务之间有依赖关系 例如,先通过用户名密码取得 Token 然后通过 Token 取得用户信息,

传统实现方法:

/// 用回调的方式封装接口
enum Api {

    /// 通过用户名密码取得一个 token
    static func token(username: String, password: String,
        success: (String) -> Void,
        failure: (Error) -> Void) { ... }

    /// 通过 token 取得用户信息
    static func userinfo(token: String,
        success: (UserInfo) -> Void,
        failure: (Error) -> Void) { ... }
        }
/// 通过用户名和密码获取用户信息
Api.token(username: "beeth0ven", password: "987654321",
    success: { token in
        Api.userInfo(token: token,
            success: { userInfo in
                print("获取用户信息成功: \(userInfo)")
            },
            failure: { error in
                print("获取用户信息失败: \(error)")
        })
    },
    failure: { error in
        print("获取用户信息失败: \(error)")
})

通过 Rx 来实现:

/// 用 Rx 封装接口
enum Api {

    /// 通过用户名密码取得一个 token
        static func token(username: String, password: String) -> Observable<String> { ... }

    /// 通过 token 取得用户信息
    static func userInfo(token: String) -> Observable<UserInfo> { ... }
}
/// 通过用户名和密码获取用户信息
Api.token(username: "beeth0ven", password: "987654321")
    .flatMapLatest(Api.userInfo)
    .subscribe(onNext: { userInfo in
        print("获取用户信息成功: \(userInfo)")
    }, onError: { error in
        print("获取用户信息失败: \(error)")
    })
        .disposed(by: disposeBag)

这样你无需嵌套太多层,从而使得代码易读,易维护。

  1. 等待多个并发任务完成后处理结果 例如,需要将两个网络请求合并成一个,

通过 Rx 来实现:

/// 用 Rx 封装接口
enum Api {

    /// 取得老师的详细信息
    static func teacher(teacherId: Int) -> Observable<Teacher> { ... }

    /// 取得老师的评论
        static func teacherComments(teacherId: Int) -> Observable<[Comment]> { ... }
        }
/// 同时取得老师信息和老师评论
Observable.zip(
      Api.teacher(teacherId: teacherId),
      Api.teacherComments(teacherId: teacherId)
    ).subscribe(onNext: { (teacher, comments) in
        print("获取老师信息成功: \(teacher)")
        print("获取老师评论成功: \(comments.count) 条")
    }, onError: { error in
        print("获取老师信息或评论失败: \(error)")
    })
    .disposed(by: disposeBag)

那么为什么要使用 RxSwift ?

Rx + MVVM

Microsoft的MVVM架构专为在提供数据绑定的平台上创建的事件驱动软件而开发。 RxSwift和MVVM绝对可以很好地协同工作,在本书的最后,您将研究该模式以及如何使用RxSwift实现它。 MVVM和RxSwift结合在一起的原因是ViewModel允许您公开Observable 属性,您可以直接绑定到View Controller胶水代码中的UIKit控件。这使得绑定模型数据到UI非常简单,无法表示和代码:

FrizzleFur commented 5 years ago

函数响应式编程

函数响应式编程是种编程范式。它是通过构建函数操作数据序列,然后对这些序列做出响应的编程方式。它结合了函数式编程以及响应式编程

函数式编程

函数式编程是种编程范式,它需要我们将函数作为参数传递,或者作为返回值返还。我们可以通过组合不同的函数来得到想要的结果。

函数式编程 -> 函数响应式编程

现在大家已经了解我们是如何运用函数式编程来操作序列的。其实我们可以把这种操作序列的方式再升华一下。例如,你可以把一个按钮的点击事件看作是一个序列:

// 假设用户在进入页面到离开页面期间,总共点击按钮 3 次

// 按钮点击序列
let taps: Array<Void> = [(), (), ()]

// 每次点击后弹出提示框
taps.forEach { showAlert() }

这样处理点击事件是非常理想的,但是问题是这个序列里面的元素(点击事件)是异步产生的,传统序列是无法描叙这种元素异步产生的情况。为了解决这个问题,于是就产生了可被监听的序列Observable。它也是一个序列,只不过这个序列里面的元素可以是同步产生的,也可以是异步产生的:

// 按钮点击序列
let taps: Observable<Void> = button.rx.tap.asObservable()

// 每次点击后弹出提示框
taps.subscribe(onNext: { showAlert() })

这里 taps 就是按钮点击事件的序列。然后我们通过弹出提示框,来对每一次点击事件做出响应。这种编程方式叫做响应式编程。我们结合函数式编程以及响应式编程就得到了函数响应式编程:

passwordOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalPasswordLength }
    .bind(to: passwordValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

我们通过不同的构建函数,来创建所需要的数据序列。最后通过适当的方式来响应这个序列。这就是函数响应式编程。

数据绑定(订阅)

在 RxSwift 里有一个比较重要的概念就是数据绑定(订阅)。就是指将可被监听的序列绑定到观察者上:

我们对比一下这两段代码:

let image: UIImage = UIImage(named: ...)
imageView.image = image

let image: Observable<UIImage> = ...
image.bind(to: imageView.rx.image)

RxSwift 核心

// Observable<String>
let text = usernameOutlet.rx.text.orEmpty.asObservable()

// Observable<Bool>
let passwordValid = text
    // Operator
    .map { $0.characters.count >= minimalUsernameLength }

// Observer<Bool>
let observer = passwordValidOutlet.rx.isHidden

// Disposable
let disposable = passwordValid
    // Scheduler 用于控制任务在那个线程队列运行
    .subscribeOn(MainScheduler.instance)
    .observeOn(MainScheduler.instance)
    .bind(to: observer)

...

// 取消绑定,你可以在退出页面时取消绑定
disposable.dispose()
FrizzleFur commented 5 years ago

Observable - 可被监听的序列

所有的事物都是序列

之前我们提到,Observable 可以用于描述元素异步产生的序列。这样我们生活中许多事物都可以通过它来表示,例如:

Observable 温度

你可以将温度看作是一个序列,然后监测这个温度值,最后对这个值做出响应。例如:当室温高于 33 度时,打开空调降温。

Observable 《海贼王》动漫

你也可以把《海贼王》的动漫看作是一个序列。然后当《海贼王》更新一集时,我们就立即观看这一集。

Observable JSON

你可以把网络请求的返回的 JSON 看作是一个序列。然后当取到 JSON 时,将它打印出来。

Observable 任务回调

你可以把任务回调看作是一个序列。当任务结束后,提示用户任务已完成。

如何创建序列

现在我们已经可以把生活中的许多事物看作是一个序列了。那么我们要怎么创建这些序列呢?

实际上,框架已经帮我们创建好了许多常用的序列。例如:button的点击,textField的当前文本,switch的开关状态,slider的当前数值等等。

另外,有一些自定义的序列是需要我们自己创建的。这里介绍一下创建序列最基本的方法,例如,我们创建一个 [0, 1, ... 8, 9] 的序列:

let numbers: Observable<Int> = Observable.create { observer -> Disposable in

    observer.onNext(0)
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    observer.onNext(5)
    observer.onNext(6)
    observer.onNext(7)
    observer.onNext(8)
    observer.onNext(9)
    observer.onCompleted()

    return Disposables.create()
}

创建序列最直接的方法就是调用 Observable.create,然后在构建函数里面描述元素的产生过程。 observer.onNext(0) 就代表产生了一个元素,他的值是 0。后面又产生了 9 个元素分别是 1, 2, ... 8, 9 。最后,用 observer.onCompleted() 表示元素已经全部产生,没有更多元素了。

你可以用这种方式来封装功能组件,例如,闭包回调:

在闭包回调中,如果任务失败,就调用 observer.onError(error!)。如果获取到目标元素,就调用 observer.onNext(jsonObject)。由于我们的这个序列只有一个元素,所以在成功获取到元素后,就直接调用 observer.onCompleted() 来表示任务结束。最后 Disposables.create { task.cancel() } 说明如果数据绑定被清除(订阅被取消)的话,就取消网络请求。

这样一来我们就将传统的闭包回调转换成序列了。然后可以用 subscribe 方法来响应这个请求的结果:

json
    .subscribe(onNext: { json in
        print("取得 json 成功: \(json)")
    }, onError: { error in
        print("取得 json 失败 Error: \(error.localizedDescription)")
    }, onCompleted: {
        print("取得 json 任务成功完成")
    })
    .disposed(by: disposeBag)

这里subscribe后面的onNext,onError, onCompleted 分别响应我们创建 json 时,构建函数里面的onNext,onError, onCompleted 事件。我们称这些事件为 Event:

Event - 事件

public enum Event<Element> {
    case next(Element)
        case error(Swift.Error)
    case completed
}

特征序列

FrizzleFur commented 5 years ago

Observer - 观察者

观察者 是用来监听事件,然后它需要这个事件做出响应。例如:弹出提示框就是观察者,它对点击按钮这个事件做出响应。

响应事件的都是观察者

在 Observable 章节,我们举了个几个例子来介绍什么是可被监听的序列。那么我们还是用这几个例子来解释一下什么是观察者:

当室温高于 33 度时,打开空调降温

1

打开空调降温就是观察者 Observer

当《海贼王》更新一集时,我们就立即观看这一集

1

观看这一集就是观察者 Observer

当取到 JSON 时,将它打印出来

1

将它打印出来就是观察者 Observer

如何创建观察者

现在我们已经知道观察者主要是做什么的了。那么我们要怎么创建它们呢?

和 Observable 一样,框架已经帮我们创建好了许多常用的观察者。例如:view 是否隐藏,button 是否可点击, label 的当前文本,imageView 的当前图片等等。

另外,有一些自定义的观察者是需要我们自己创建的。这里介绍一下创建观察者最基本的方法,例如,我们创建一个弹出提示框的的观察者:

tap.subscribe(onNext: { [weak self] in self?.showAlert() }, onError: { error in print("发生错误: (error.localizedDescription)") }, onCompleted: { print("任务完成") })

AnyObserver

AnyObserver 可以用来描叙任意一种观察者。

例如: 打印网络请求结果:

URLSession.shared.rx.data(request: URLRequest(url: url))
    .subscribe(onNext: { data in
        print("Data Task Success with count: \(data.count)")
    }, onError: { error in
        print("Data Task Error: \(error)")
    })
    .disposed(by: disposeBag)

//可以看作是:

let observer: AnyObserver<Data> = AnyObserver { (event) in
    switch event {
    case .next(let data):
        print("Data Task Success with count: \(data.count)")
    case .error(let error):
        print("Data Task Error: \(error)")
    default:
        break
    }
}

URLSession.shared.rx.data(request: URLRequest(url: url))
    .subscribe(observer)
    .disposed(by: disposeBag)

Binder

Binder 主要有以下两个特征:

由于这个观察者是一个 UI 观察者,所以它在响应事件时,只会处理 next 事件,并且更新 UI 的操作需要在主线程上执行。

因此一个更好的方案就是使用 Binder:

let observer: Binder<Bool> = Binder(usernameValidOutlet) { (view, isHidden) in
    view.isHidden = isHidden
}

usernameValid
    .bind(to: observer)
    .disposed(by: disposeBag)
FrizzleFur commented 5 years ago

Subjects

在我们所遇到的事物中,有一部分非常特别。它们既是可被监听的序列也是观察者。

例如:textField的当前文本。它可以看成是由用户输入,而产生的一个文本序列。也可以是由外部文本序列,来控制当前显示内容的观察者:

// 作为可被监听的序列
let observable = textField.rx.text
observable.subscribe(onNext: { text in show(text: text) })
// 作为观察者
let observer = textField.rx.text
let text: Observable<String?> = ...
text.bind(to: observer)

有许多 UI 控件都存在这种特性,例如:switch的开关状态,segmentedControl的选中索引号,datePicker的选中日期等等。

Subject 可以看做是一种代理和桥梁。它既是订阅者又是订阅源,这意味着它既可以订阅其他 Observable 对象,同时又可以对它的订阅者们发送事件。

如果把 Observable 理解成不断输出事件的水管,那 Subject 就是套在上面的水龙头。它既怼着一根不断出水的水管,同时也向外面输送着新鲜水源。如果你直接用水杯接着水管的水,那可能导出来什么王水胶水完全把持不住;如果你在水龙头下面接着水,那你可以随心所欲的调成你想要的水速和水温。

AsyncSubject

AsyncSubject 将在源 Observable 产生完成事件后,发出最后一个元素(仅仅只有最后一个元素),如果源 Observable 没有发出任何元素,只有一个完成事件。那 AsyncSubject 也只有一个完成事件。

它会对随后的观察者发出最终元素。如果源 Observable 因为产生了一个 error 事件而中止, AsyncSubject 就不会发出任何元素,而是将这个 error 事件发送出来。

演示

let disposeBag = DisposeBag()
let subject = AsyncSubject<String>()

subject
  .subscribe { print("Subscription: 1 Event:", $0) }
  .disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")
subject.onNext("🐹")
subject.onCompleted()

输出结果:

Subscription: 1 Event: next(🐹)
Subscription: 1 Event: completed

PublishSubject

PublishSubject 会发送订阅者从订阅之后的事件序列

演示


let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject
  .subscribe { print("Subscription: 1 Event:", $0) }
  .disposed(by: disposeBag)

subject.onNext("🐶")
subject.onNext("🐱")

subject
  .subscribe { print("Subscription: 2 Event:", $0) }
  .disposed(by: disposeBag)

subject.onNext("🅰️")
subject.onNext("🅱️")
//输出结果:

Subscription: 1 Event: next(🐶)
Subscription: 1 Event: next(🐱)
Subscription: 1 Event: next(🅰️)
Subscription: 2 Event: next(🅰️)
Subscription: 1 Event: next(🅱️)
Subscription: 2 Event: next(🅱️)

如果源 Observable 因为产生了一个 error 事件而中止, PublishSubject 就不会发出任何元素,而是将这个 error 事件发送出来。

BehaviorSubject

BehaviorSubject 在新的订阅对象订阅的时候会发送最近发送的事件,如果没有则发送一个默认值。 所以初始化一个BehaviorSubject一定要有一个初始值。 应用:比如,默认bool属性

example("BehaviorSubject") {
    let subject = BehaviorSubject(value: "z")
    writeSequenceToConsole("1", sequence: subject)
    subject.on(.Next("a"))
    subject.on(.Next("b"))
    writeSequenceToConsole("2", sequence: subject)
    subject.on(.Next("c"))
    subject.on(.Completed)
}

--- BehaviorSubject example ---
Subscription: 1, event: Next(z)
Subscription: 1, event: Next(a)
Subscription: 1, event: Next(b)
Subscription: 2, event: Next(b)
Subscription: 1, event: Next(c)
Subscription: 2, event: Next(c)
Subscription: 1, event: Completed
Subscription: 2, event: Completed

如果源 Observable 因为产生了一个 error 事件而中止, BehaviorSubject 就不会发出任何元素,而是将这个 error 事件发送出来。

ReplaySubject

ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。

这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察者。 bufferSize可以控制订阅前的信号个数。

error: 如前文所说,当你向一个ReplaySubject发送一个completed或error事件,会导致它的数据流终结,此后,若再订阅它,ReplaySubject会先向订阅者发送缓冲区内的所有元素,再将导致它终结的completed或者error事件发送给订阅者。 dispose: ReplaySubject在析构以后的表现和前两个subjects一样,它会通知既订阅者调用它们订阅了的onDisposed:{}“事件”里的闭包(假如订阅了的话)。而对于新来的订阅者,它不会像对待completed和error那样发送缓冲区内的元素,而是仅仅抛出一个“对象已析构”的错误。

Variable

Variable 是基于 BehaviorSubject 的一层封装,它的优势是:不会被显式终结。即:不会收到 .Completed 和 .Error 这类的终结事件,它会主动在析构的时候发送 .Complete 。

example("Variable") {
    let variable = Variable("z")
    writeSequenceToConsole("1", sequence: variable)
    variable.value = "a"
        variable.value = "b"
    writeSequenceToConsole("2", sequence: variable)
    variable.value = "c"
}

--- Variable example ---
Subscription: 1, event: Next(z)
Subscription: 1, event: Next(a)
Subscription: 1, event: Next(b)
Subscription: 2, event: Next(b)
Subscription: 1, event: Next(c)
Subscription: 2, event: Next(c)
Subscription: 1, event: Completed
Subscription: 2, event: Completed

As mentioned earlier, a Variable wraps a BehaviorSubject and stores its current value as state. You can access that current value via its value property, and, unlike other subjects and observables in general, you also use that value property to set a new element onto a variable. In other words, you don’t use onNext(_:).

我们来对比一下 var 以及 Variable 的用法:

使用 var:

// 在 ViewController 中
var model: Model? = nil {
    didSet { updateUI(with: model) }
}

override func viewDidLoad() {
    super.viewDidLoad()

    model = getModel()
}

func updateUI(with model: Model?) { ... }
func getModel() -> Model { ... }
使用 Variable:

// 在 ViewController 中
let model: Variable<Model?> = Variable(nil)

override func viewDidLoad() {
    super.viewDidLoad()

    model.asObservable()
        .subscribe(onNext: { [weak self] model in
            self?.updateUI(with: model)
        })
        .disposed(by: disposeBag)

    model.value = getModel()
}

func updateUI(with model: Model?) { ... }
func getModel() -> Model { ... }

第一种使用 var 的方式十分常见,在 ViewController 中监听 Model 的变化,然后刷新页面。

第二种使用 Variable 则是 RxSwift 独有的。Variable 几乎提供了 var 的所有功能。另外,加上一条非常重要的特性,就是可以通过调用 asObservable() 方法转换成序列。然后你可以对这个序列应用操作符,来合成其他的序列。所以,如果我们声明的变量需要提供 Rx 支持,那就选用 Variable 这个类型。

总结

FrizzleFur commented 5 years ago

Operator - 操作符

我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控制红色提示语是否隐藏。我们还通过 combineLatest 方法,将用户名是否有效密码是否有效合并成两者是否同时有效。然后用这个合成后来的序列来控制按钮是否可点击。

这里 map 和 combineLatest 都是操作符,它们可以帮助我们构建所需要的序列。现在,我们再来看几个例子:

filter - 过滤

你可以用 filter 创建一个新的序列。这个序列只发出温度大于 33 度的元素。

map - 转换

你可以用 map 创建一个新的序列。这个序列将原有的 JSON 转换成 Model 。这种转换实际上就是解析 JSON 。

zip - 配对

你可以用 zip 来合成一个新的序列。这个序列将汉堡序列的元素和薯条序列的元素配对后,生成一个新的套餐序列。

如何使用操作符

使用操作符是非常容易的。你可以直接调用实例方法,或者静态方法:

决策树

Rx 提供了充分的操作符来帮我们创建序列。当然如果内置操作符无法满足你的需求时,你还可以创建自定义的操作符。

如果你不确定该如何选择操作符,可以参考 决策树。它会引导你找出合适的操作符。

操作符列表

26个英文字母我都认识,可是连成一个句子我就不怎么认得了...

这里提供一个操作符列表,它们就好比是26个英文字母。你如果要将它们的作用全部都发挥出来,是需要学习如何将它们连成一个句子的:

FrizzleFur commented 5 years ago

Disposable - 可被清除的资源

通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清除的资源(Disposable) 调用 dispose 方法:

通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清除的资源(Disposable) 调用 dispose 方法:

var disposable: Disposable?

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    self.disposable = textField.rx.text.orEmpty
        .subscribe(onNext: { text in print(text) })
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    self.disposable?.dispose()
}

调用 dispose 方法后,订阅将被取消,并且内部资源都会被释放。通常情况下,你是不需要手动调用 dispose 方法的,这里只是做个演示而已。我们推荐使用 清除包(DisposeBag) 或者 takeUntil 操作符 来管理订阅的生命周期。

DisposeBag - 清除包

用 ARC 来管理订阅的生命周期了

var disposeBag = DisposeBag()

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    textField.rx.text.orEmpty
        .subscribe(onNext: { text in print(text) })
        .disposed(by: self.disposeBag)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    self.disposeBag = DisposeBag()
}

当 清除包 被释放的时候,清除包 内部所有 可被清除的资源(Disposable) 都将被清除。在输入验证中我们也多次看到 清除包 的身影:

var disposeBag = DisposeBag() // 来自父类 ViewController

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    usernameValid
        .bind(to: usernameValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    passwordValid
        .bind(to: passwordValidOutlet.rx.isHidden)
        .disposed(by: disposeBag)

    everythingValid
        .bind(to: doSomethingOutlet.rx.isEnabled)
        .disposed(by: disposeBag)

    doSomethingOutlet.rx.tap
        .subscribe(onNext: { [weak self] in self?.showAlert() })
        .disposed(by: disposeBag)
}

这个例子中 disposeBag 和 ViewController 具有相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的 5 次绑定(订阅)也就被取消了。这正是我们所需要的。

takeUntil

另外一种实现自动取消订阅的方法就是使用 takeUntil 操作符,上面那个输入验证的演示代码也可以通过使用 takeUntil 来实现:

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    _ = usernameValid
        .takeUntil(self.rx.deallocated)
        .bind(to: passwordOutlet.rx.isEnabled)

    _ = usernameValid
        .takeUntil(self.rx.deallocated)
        .bind(to: usernameValidOutlet.rx.isHidden)

    _ = passwordValid
        .takeUntil(self.rx.deallocated)
        .bind(to: passwordValidOutlet.rx.isHidden)

    _ = everythingValid
        .takeUntil(self.rx.deallocated)
        .bind(to: doSomethingOutlet.rx.isEnabled)

    _ = doSomethingOutlet.rx.tap
        .takeUntil(self.rx.deallocated)
        .subscribe(onNext: { [weak self] in self?.showAlert() })
}

🌰

        NotificationCenter.default.rx
            .notification(UITextView.keyboardWillHideNotification)
            .takeUntil(self.rx.deallocated)
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] notification in self?.keyboardWillDisappear(notification) })

这将使得订阅一直持续到控制器的 dealloc 事件产生为止。

FrizzleFur commented 5 years ago

Schedulers - 调度器

![](https://pic-mike.oss-cn-hongkong.aliyuncs.com/ Blog/20190503140846.png)

Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。

如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:


// 后台取得数据,主线程处理结果
DispatchQueue.global(qos: .userInitiated).async {
    let data = try? Data(contentsOf: url)
    DispatchQueue.main.async {
        self.data = data
    }
}

如果用 RxSwift 来实现,大致是这样的:


let rxData: Observable<Data> = ...

rxData
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
    .observeOn(MainScheduler.instance)
        .subscribe(onNext: { [weak self] data in
        self?.data = data
    })
    .disposed(by: disposeBag)

使用 subscribeOn

我们用 subscribeOn 来决定数据序列的构建函数在哪个 Scheduler 上运行。以上例子中,由于获取 Data 需要花很长的时间,所以用 subscribeOn 切换到 后台 Scheduler 来获取 Data。这样可以避免主线程被阻塞。

使用 observeOn

我们用 observeOn 来决定在哪个 Scheduler 监听这个数据序列。以上例子中,通过使用 observeOn 方法切换到主线程来监听并且处理结果。

一个比较典型的例子就是,在后台发起网络请求,然后解析数据,最后在主线程刷新页面。你就可以先用 subscribeOn 切到后台去发送请求并解析数据,最后用 observeOn 切换到主线程更新页面。

FrizzleFur commented 5 years ago

Error Handling - 错误处理

一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

retry 可以让序列在发生错误后重试:

// 请求 JSON 失败时,立即重试,
// 重试 3 次后仍然失败,就将错误抛出

let rxJson: Observable<JSON> = ...

rxJson
    .retry(3)
    .subscribe(onNext: { json in
        print("取得 JSON 成功: \(json)")
    }, onError: { error in
        print("取得 JSON 失败: \(error)")
    })
    .disposed(by: disposeBag)

以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

retryWhen

如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

// 请求 JSON 失败时,等待 5 秒后重试,

let retryDelay: Double = 5  // 重试延时 5 秒

rxJson
    .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
        return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
    }
    .subscribe(...)
    .disposed(by: disposeBag)

这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
    ...
    }

闭包里面的参数是 Observable 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

如果需要加上一个最大重试次数的限制:

// 请求 JSON 失败时,等待 5 秒后重试,
// 重试 4 次后仍然失败,就将错误抛出

let maxRetryCount = 4       // 最多重试 4 次
let retryDelay: Double = 5  // 重试延时 5 秒

rxJson
    .retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
        return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
            guard index < maxRetryCount else {
                return Observable.error(error)
            }
                        return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
        }
    }
    .subscribe(...)
    .disposed(by: disposeBag)

我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:

...
rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
    guard index < maxRetryCount else {
        return Observable.error(error)
    }
        return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
...

我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。

catchError - 恢复

catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

searchBar.rx.text.orEmpty
    ...
    .flatMapLatest { query -> Observable<[Repository]> in
            ...
        return searchGitHub(query)
            .catchErrorJustReturn([])
    }
    ...
    .bind(to: ...)
    .disposed(by: disposeBag)

我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

// 先从网络获取数据,如果获取失败了,就从本地缓存获取数据

let rxData: Observable<Data> = ...      // 网络请求的数据
let cahcedData: Observable<Data> = ...  // 之前本地缓存的数据

rxData
    .catchError { _ in cahcedData }
    .subscribe(onNext: { date in
            print("获取数据成功: \(date.count)")
    })
    .disposed(by: disposeBag)

Result

如果我们只是想给用户错误提示,那要如何操作呢?

以下提供一个最为直接的方案,不过这个方案存在一些问题:

// 当用户点击更新按钮时,
// 就立即取出修改后的用户信息。
// 然后发起网络请求,进行更新操作,
// 一旦操作失败就提示用户失败原因

updateUserInfoButton.rx.tap
    .withLatestFrom(rxUserInfo)
    .flatMapLatest { userInfo -> Observable<Void> in
        return update(userInfo)
    }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: {
        print("用户信息更新成功")
            }, onError: { error in
        print("用户信息更新失败: \(error.localizedDescription)")
    })
    .disposed(by: disposeBag)

这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如使用枚举 Result:

// 自定义一个枚举类型 Result
public enum Result<T> {
    case success(T)
    case failure(Swift.Error)
}

然后之前的代码需要修改成:

updateUserInfoButton.rx.tap
    .withLatestFrom(rxUserInfo)
    .flatMapLatest { userInfo -> Observable<Result<Void>> in
        return update(userInfo)
            .map(Result.success)  // 转换成 Result
            .catchError { error in Observable.just(Result.failure(error)) }
    }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { result in
        switch result {           // 处理 Result
        case .success:
            print("用户信息更新成功")
        case .failure(let error):
            print("用户信息更新失败: \(error.localizedDescription)")
                    }
    })
    .disposed(by: disposeBag)

这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。就算网络请求失败,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!

FrizzleFur commented 5 years ago

如何选择操作符?

下面这个决策树可以帮助你找到需要的操作符。


决策树

我想要创建一个 Observable

我想要创建一个 Observable 通过组合其他的 Observables

我想要转换 Observable 的元素后,再将它们发出来

我想要将产生的每一个元素,拖延一段时间后再发出:delay

我想要将产生的事件封装成元素发送出来

我想要忽略掉所有的 next 事件,只接收 completed 和 error 事件:ignoreElements

我想创建一个新的 Observable 在原有的序列前面加入一些元素:startWith

我想从 Observable 中收集元素,缓存这些元素之后在发出:buffer

我想将 Observable 拆分成多个 Observableswindow

我想只接收 Observable 中特定的元素

我想重新从 Observable 中发出某些元素

我想要从一些 Observables 中,只取第一个产生元素的 Observableamb

我想评估 Observable 的全部元素

我想把 Observable 转换为其他的数据结构:as...

我想在某个 Scheduler 应用操作符:subscribeOn

我想要 Observable 发生某个事件时, 采取某个行动:do

我想要 Observable 发出一个 error 事件:error

我想要 Observable 发生错误时,优雅的恢复

我创建一个 Disposable 资源,使它与 Observable 具有相同的寿命:using

我创建一个 Observable,直到我通知它可以产生元素后,才能产生元素:publish

FrizzleFur commented 5 years ago

冷热观察者

恕我直言,我建议更多地将其视为序列的属性而不是单独的类型,因为它们由完全适合它们的相同抽象表示,Observable序列。

这是ReactiveX.io的定义

Observable何时开始发出其物品序列?这取决于Observable。“热”Observable可以在创建项目后立即开始发出项目,因此任何后来订阅该Observable的观察者都可以开始在中间某处观察序列。另一方面,“冷”Observable等待观察者在开始发射物品之前订阅它,因此这样的观察者保证从一开始就看到整个序列。

热的观察者 冷观察
......是序列 ......是序列
无论是否有任何观察者订阅,都要使用资源(“产生热量”)。 在观察者订阅之前,不要使用资源(不产生热量)。
变量/属性/常量,抽头坐标,鼠标坐标,UI控件值,当前时间 异步操作,HTTP连接,TCP连接,流
通常包含~N个元素 通常包含~1个元素
无论是否有任何观察者订阅,都会产生序列元素。 仅当存在订阅的观察者时才生成序列元素。
序列计算资源通常在所有订阅的观察者之间共享。 通常为每个订阅的观察者分配序列计算资源。
通常是有状态的 通常无状态
FrizzleFur commented 5 years ago

ImagePicker - 图片选择器

这是一个图片选择器的演示,你可以在这里下载这个例子


简介

这个 App 主要有这样几个交互:

整体结构

...
override func viewDidLoad() {
    super.viewDidLoad()

    ...

    cameraButton.rx.tap
        .flatMapLatest { [weak self] _ in
            return UIImagePickerController.rx.createWithParent(self) { picker in
                picker.sourceType = .camera
                picker.allowsEditing = false
            }
            .flatMap { $0.rx.didFinishPickingMediaWithInfo }
            .take(1)
        }
        .map { info in
            return info[UIImagePickerControllerOriginalImage] as? UIImage
        }
        .bind(to: imageView.rx.image)
        .disposed(by: disposeBag)

    ...    
}

我们忽略一些细节,看一下序列的转换过程:

cameraButton.rx.tap
    .flatMapLatest { () -> Observable<[String: AnyObject]> ... } // 点击 -> 图片信息
    .map { [String : AnyObject] -> UIImage? ... } // 图片信息 -> 图片
    .bind(to: imageView.rx.image) // 数据绑定
    .disposed(by: disposeBag)

最开始的按钮点击是一个 Void 序列,接着用 flatMapLatest 将它异步转化为图片信息序列 [String : AnyObject],然后用 map 同步的从图片信息中取出图片,从而得到了一个图片序列 UIImage?,最后将这个图片序列绑定到 imageView 上:

这是相机按钮点击后需要执行的操作。另外两个按钮(相册和裁剪)和它十分相似,只不过传入了不同的参数,通过不同的键取出图片:

...
override func viewDidLoad() {
    super.viewDidLoad()

    ...

    // 相机
    cameraButton.rx.tap
        .flatMapLatest { [weak self] _ in
            return UIImagePickerController.rx.createWithParent(self) { picker in
                picker.sourceType = .camera
                picker.allowsEditing = false
            }
            .flatMap { $0.rx.didFinishPickingMediaWithInfo }
            .take(1)
        }
        .map { info in
            return info[UIImagePickerControllerOriginalImage] as? UIImage
        }
        .bind(to: imageView.rx.image)
        .disposed(by: disposeBag)

    // 相册
    galleryButton.rx.tap
        .flatMapLatest { [weak self] _ in
            return UIImagePickerController.rx.createWithParent(self) { picker in
                picker.sourceType = .photoLibrary
                picker.allowsEditing = false
            }
            .flatMap {
                $0.rx.didFinishPickingMediaWithInfo
            }
            .take(1)
        }
        .map { info in
            return info[UIImagePickerControllerOriginalImage] as? UIImage
        }
        .bind(to: imageView.rx.image)
        .disposed(by: disposeBag)

    // 裁剪
    cropButton.rx.tap
        .flatMapLatest { [weak self] _ in
            return UIImagePickerController.rx.createWithParent(self) { picker in
                picker.sourceType = .photoLibrary
                picker.allowsEditing = true
            }
            .flatMap { $0.rx.didFinishPickingMediaWithInfo }
            .take(1)
        }
        .map { info in
            return info[UIImagePickerControllerEditedImage] as? UIImage
        }
        .bind(to: imageView.rx.image)
        .disposed(by: disposeBag)
}
...

参考

FrizzleFur commented 5 years ago

Driver

1,基本介绍

(1)Driver 可以说是最复杂的 trait,它的目标是提供一种简便的方式在 UI 层编写响应式代码。

(2)如果我们的序列满足如下特征,就可以使用它:

2,为什么要使用 Driver?

(1)Driver 最常使用的场景应该就是需要用序列来驱动应用程序的情况了,比如:

(2)与普通的操作系统驱动程序一样,如果出现序列错误,应用程序将停止响应用户输入。

(3)在主线程上观察到这些元素也是极其重要的,因为 UI 元素和应用程序逻辑通常不是线程安全的。

(4)此外,使用构建 Driver 的可观察的序列,它是共享状态变化。

3,使用样例

       这个是官方提供的样例,大致的意思是根据一个输入框的关键字,来请求数据,然后将获取到的结果绑定到另一个 LabelTableView 中。

(1)初学者使用 Observable 序列加 bindTo 绑定来实现这个功能的话可能会这么写:

let results = query.rx.text
    .throttle(0.3, scheduler: MainScheduler.instance) //在主线程中操作,0.3秒内值若多次改变,取最后一次
    .flatMapLatest { query in //筛选出空值, 拍平序列
        fetchAutoCompleteItems(query) //向服务器请求一组结果
}

//将返回的结果绑定到用于显示结果数量的label上
results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

//将返回的结果绑定到tableView上
results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

但这个代码存在如下 3 个问题:

(2)把上面几个问题修改后的代码是这样的:

let results = query.rx.text
    .throttle(0.3, scheduler: MainScheduler.instance)//在主线程中操作,0.3秒内值若多次改变,取最后一次
    .flatMapLatest { query in //筛选出空值, 拍平序列
        fetchAutoCompleteItems(query)   //向服务器请求一组结果
            .observeOn(MainScheduler.instance)  //将返回结果切换到到主线程上
            .catchErrorJustReturn([])       //错误被处理了,这样至少不会终止整个序列
    }
    .shareReplay(1)                //HTTP 请求是被共享的

//将返回的结果绑定到显示结果数量的label上
results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

//将返回的结果绑定到tableView上
results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

        虽然我们通过增加一些额外的处理,让程序可以正确运行。到对于一个大型的项目来说,如果都这么干也太麻烦了,而且容易遗漏出错。

(3)而如果我们使用 Driver 来实现的话就简单了,代码如下:

代码讲解:
(1)首先我们使用 asDriver 方法将 ControlProperty 转换为 Driver。
(2)接着我们可以用 .asDriver(onErrorJustReturn: []) 方法将任何 Observable 序列都转成 Driver,因为我们知道序列转换为 Driver 要他满足 3 个条件:

*   不会产生 error 事件
*   一定在主线程监听(MainScheduler)
*   共享状态变化(shareReplayLatestWhileConnected)

而 asDriver(onErrorJustReturn: []) 相当于以下代码:
        let safeSequence = xs
         .observeOn(MainScheduler.instance) // 主线程监听
         .catchErrorJustReturn(onErrorJustReturn) // 无法产生错误
         .share(replay: 1, scope: .whileConnected)// 共享状态变化
         return Driver(raw: safeSequence) // 封装
(3)同时在 Driver 中,框架已经默认帮我们加上了 shareReplayLatestWhileConnected,所以我们也没必要再加上"replay"相关的语句了。
(4)最后记得使用 drive 而不是 bindTo
let results = query.rx.text.asDriver()        // 将普通序列转换为 Driver
    .throttle(0.3, scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchAutoCompleteItems(query)
            .asDriver(onErrorJustReturn: [])  // 仅仅提供发生错误时的备选返回值
    }

//将返回的结果绑定到显示结果数量的label上
results
    .map { "\($0.count)" }
    .drive(resultCount.rx.text) // 这里使用 drive 而不是 bindTo
    .disposed(by: disposeBag)

//将返回的结果绑定到tableView上
results
    .drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { //  同样使用 drive 而不是 bindTo
        (_, result, cell) in
        cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

由于 drive 方法只能被 Driver 调用。这意味着,如果代码存在 drive,那么这个序列不会产生错误事件并且一定在主线程监听。这样我们就可以安全的绑定 UI元素。