RXSwift的教程太多, ReactiveSwift的教程又太少
前言
大概是這樣, Swift4.0出了, 重新梳理Swift知識, 對比了下RXSwift和ReactiveSwift, 喜歡ReactiveSwift多一些, 想了想, 出份基礎教程. 建議新人朋友只看如何使用, 至於實現概述看看最後的總結和圖瞭解一下思路就行了.
目錄
- Event
- Observer
- Signal
- SignalProducer
- Property/MutableProperty
- Action/CocoaAction
- Hello ReactiveSwift
Event
在ReactiveSwift中, 資訊的載體(value/error)對應的是一個列舉Event, Event的定義如下:
//Event.swift
public enum Event {
case value(Value)
case failed(Error)
case completed
case interrupted
}
複製程式碼
前三個狀態顧名思義就不解釋了, 第四個狀態interrupted表示事件被打斷, 除非你去訂閱一個已經無效的訊號, 否則這個狀態不會出現, 所以不用太多關注這個狀態.
需要注意的點: 當訊號傳送非Value的Event時, 那麼這個訊號就無效了. 無效的原因可能是failed(失敗), completed(壽終正寢), interrupted(早就無效了).
Observer
上面說過Event是資訊的載體, 而這裡的Observer則是資訊的處理邏輯封裝. Observer的主要程式碼如下:
//Observer.swift
public final class Observer {
public typealias Action = (Event) -> Void
private let _send: Action
public init(_ action: @escaping Action) {
self._send = action
...
}
public func send(_ event: Event) {
_send(event)
}
public func send(value: Value) {
_send(.value(value))
}
public func sendXXX() //其實都是send(_ event: Event)
}
複製程式碼
容易看到, Observer內部保持了一個處理Event的閉包, 初始化Observer就是在設定這個閉包, 而呼叫Observer.send則是在執行這個閉包.
需要注意的點: Observer封裝了Event的處理邏輯.
Signal
有了資訊的載體和資訊的處理邏輯, 接下來需要的是: 將資訊傳送出去. 在ReactiveSwift中, 想要傳送資訊共有四種途徑, 這裡我們先介紹第一種: Signal.(事實上, 四種途徑最終都是通過Signal來完成的, 所以, 其實只有一種.)
Signal是ReactiveSwift中熱訊號的實現, "熱"的意思是它是一直活動著的, 會主動將產出的事件Event向外傳送, 而不會等到有人訂閱後才開始傳送. 這意味著如果訂閱的時機晚於傳送的時機, 那麼訂閱者是不會收到訂閱時機之前的事件的. 舉個栗子: 春晚現場直播從晚8點一直播到12點, 這段時間產出的節目就是Value事件, 12點一到產出的就是Completed事件. 很明顯, 不管有沒有人看春晚, 春晚現場都不關心, 節目來了就上, 時間一到就散. 但如果你想看直播, 最好的時機當然是8點, 若是9點才開啟電視, 那9點之前的節目你肯定就錯過了.
概念講完了, 我們來看看程式碼, 這裡我會分成兩個部分: Signal的使用和Signal的實現概述, 大家主要關注使用部分即可.
- Signal的使用
note: 這裡的Value和Error都是泛型, 你需要在建立的時候進行指定
//public static func pipe(disposable: Disposable? = nil) -> (output: Signal, input: Observer)
let signalTuple = Signal<Int, NoError>.pipe()
let (signal, observer) = Signal<Int, NoError>.pipe()
...
複製程式碼
通常, 你應該只通過Signal.pipe()函式來初始化一個熱訊號. 這個函式會返回一個元組, 元組的第一個值是output(型別為Signal), 第二個值是input(型別為Observer). 我們通過output來訂閱訊號, 通過input來向訊號發生資訊.
需要注意的點: output的作用是管理訊號狀態並儲存由訂閱者提供的Observer物件(Observer._send封裝了Event的處理邏輯), 而input的作用則是在接收到Event後依次執行這些被儲存的Observer._send. 來看一段訂閱Signal的基礎程式碼:
override func viewDidLoad() {
super.viewDidLoad()
//1.建立signal(output)和innerObserver(input)
let (signal, innerObserver) = Signal<Int, NoError>.pipe()
//2.建立Observer
let outerObserver1 = Signal<Int, NoError>.Observer(value: { (value) in
print("did received value: \(value)")
})
//2.還是建立Observer
let outerObserver2 = Signal<Int, NoError>.Observer { (event) in
switch event {
case let .value(value):
print("did received value: \(value)")
default: break
}
}
signal.observe(outerObserver1)//3.向signal中新增Observer
signal.observe(outerObserver2)//3.還是向signal中新增Observer
innerObserver.send(value: 1)//4.向signal發生資訊(執行signal儲存的所有Observer物件的Event處理邏輯)
innerObserver.sendCompleted()//4.還是執向signal發生資訊
}
//輸出: did received value: 1
did received value: 1
複製程式碼
這段程式碼簡單的演示瞭如何建立並訂閱一個Signal, 但事實上, 實際開發中我們肯定不會這樣寫, 太繁瑣了. 它的意義在於告訴各位:
1)每訂閱一次Signal實際上就是在向Signal中新增一個Observer物件.
2)即使每次訂閱訊號的處理邏輯都是一樣的, 但它們仍然是完全不同的的兩個Observer物件.
我們來把上面的程式碼改的簡潔一點:
typealias NSignal<T> = ReactiveSwift.Signal<T, NoError>
override func viewDidLoad() {
super.viewDidLoad()
//1.建立signal(output)和innerObserver(input)
let (signal, innerObserver) = NSignal<Int>.pipe()
signal.observeValues { (value) in //2&3.建立Observer並新增到Signal中
print("did received value: \(value)")
}
signal.observeValues { (value) in //2&3.還是建立Observer並新增到Signal中
print("did received value: \(value)")
}
innerObserver.send(value: 1) //4. ...
innerObserver.sendCompleted() //4. ...
}
複製程式碼
例子很簡單, 主要介紹下Signal.observeValues, 這是Signal.observe的一個便利函式, 作用是建立一個只處理Value事件的Observer並新增到Signal中, 類似的還有只處理Failed事件的Signal.observeFailed和所有事件都能處理的Signal.observeResult.
吐槽: ReactiveSwift中職責區分十分明確, 這意味著做一件簡單的事情可能會需要多個部件共同協作, 這對使用者來說比較繁瑣, 所以ReactiveSwift提供了很多的便利函式來進行簡化, 但過多的便利函式密密麻麻一堆讓人看得心煩, 於是被剛接觸的朋友們吐槽"複雜", "不簡潔". 但通常我們只需要記著幾個常用函式即可做好大部分事情, 其實ReactiveSwift非常簡潔.
回到程式碼來, 接下來介紹下"熱"訊號的相關程式碼:
typealias NSignal<T> = ReactiveSwift.Signal<T, NoError>
複製程式碼
//ViewModel.swift
class ViewModel {
let signal: NSignal<Int>
let innerObserver: NSignal<Int>.Observer
init() { (signal, innerObserver) = NSignal<Int>.pipe() }
}
//View1.swift
class View1 {
func bind(viewModel: ViewModel) {
viewModel.signal.observeValues { (value) in
print("View1 received value: \(value)")
}
}
}
//View2.swift
class View2 {
func bind(viewModel: ViewModel) {
viewModel.signal.observeValues { (value) in
print("View2 received value: \(value)")
}
}
}
//View3.swift
class View3 {
func bind(viewModel: ViewModel) {
viewModel.signal.observeValues { (value) in
print("View3 received value: \(value)")
}
viewModel.signal.observeInterrupted {
print("View3 received interrupted")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let view1 = View1()
let view2 = View2()
let view3 = View3()
let viewModel = ViewModel()
view1.bind(viewModel: viewModel)//訂閱時機較早
viewModel.innerObserver.send(value: 1)
view2.bind(viewModel: viewModel)//訂閱時機較晚
viewModel.innerObserver.send(value: 2)
viewModel.innerObserver.sendCompleted()//傳送一個非Value事件 訊號無效
view3.bind(viewModel: viewModel)//訊號無效後才訂閱
viewModel.innerObserver.send(value: 3)//訊號無效後傳送事件
}
輸出: View1 received value: 1
View1 received value: 2
View2 received value: 2
View3 received interrupted
複製程式碼
這裡我們可以看到, view2的訂閱時間晚於value1的傳送時間, 所以view2收不到value1對應的事件, 這部分對應上面我說的熱訊號並不關心訂閱者的情況, 一旦有事件即會傳送. 第二部分則是Signal自身的特性: 收到任何非Value的事件後訊號便無效了. 所以你會看到雖然view1和view2的訂閱都早於value3的傳送時間, 但因為value3在訊號傳送前先傳送了completed事件, 所以view1和view2都不會收到value3事件, 同理, view3也不會收到value3事件(它只會收到一個interrupted, 如果它關心的話).
接下來介紹一些Signal常用的函式, 這些函式會在文末的demo中出現.
- KVO
public func signal(forKeyPath keyPath: String) -> Signal<Any?, NoError>
複製程式碼
let tableView: UITableView
dynamic var someValue = 0
reactive.signal(forKeyPath: "someValue").observeValues { [weak self] (value) in
//code
}
tableView.reactive.signal(forKeyPath: "contentSize").observeValues {[weak self] (contentSize) in
if let contentSize = contentSize as? CGSize,
let strongSelf = self {
let isHidden = contentSize.height < strongSelf.tableView.height
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now(), execute: {
strongSelf.tableView.mj_footer.isHidden = isHidden
})
}
}
複製程式碼
KVO的Reactive版本, 對於NSObject的子類可以直接使用, 對於Swift的原生類需要加上dynamic修飾.
- map
let (signal, innerObserver) = NSignal<Int>.pipe()
signal.map { return "xxx" + String($0) } //map就不解釋了
.observeValues { (value) in
print(value)
}
innerObserver.send(value: 1)
innerObserver.sendCompleted()
輸出: xxx1
複製程式碼
- on
public func on(
event: ((Event) -> Void)? = nil,
failed: ((Error) -> Void)? = nil,
completed: (() -> Void)? = nil,
interrupted: (() -> Void)? = nil,
terminated: (() -> Void)? = nil,
disposed: (() -> Void)? = nil,
value: ((Value) -> Void)? = nil) -> Signal<Value, Error>
複製程式碼
let (signal, innerObserver) = NSignal<Int>.pipe()
signal.on( value: { (value) in
print("on value: \(value)")
}).observeValues { (value) in
print("did received value: \(value)")
}
innerObserver.send(value: 1)
innerObserver.sendCompleted()
輸出: on value: 1
did received value: 1
複製程式碼
on: 在訊號傳送事件和訂閱者收到事件之間插入一段事件處理邏輯, 你可以把它看做map的簡潔版. (這個函式的引數很多, 但預設都有給nil, 所以你只需要關心自己需要的部分即可, 比如這裡我只想在Value事件間插入邏輯)
- take(until:)
public func take(until trigger: Signal<(), NoError>) -> Signal<Value, Error>
複製程式碼
let (signal, innerObserver) = NSignal<Int>.pipe()
let (takeSignal, takeObserver) = NSignal<()>.pipe()
signal.take(until: takeSignal).observeValues { (value) in
print("received value: \(value)")
}
innerObserver.send(value: 1)
innerObserver.send(value: 2)
takeObserver.send(value: ())
innerObserver.send(value: 3)
takeObserver.sendCompleted()
innerObserver.sendCompleted()
輸出: received value: 1
received value: 2
複製程式碼
take(until:): 在takeSignal傳送Event之前, signal可以正常傳送Event, 一旦takeSignal開始傳送Event, signal就停止傳送, takeSignal相當於一個停止標誌位.
- take(first:)
public func take(first count: Int) -> Signal<Value, Error>
複製程式碼
let (signal, innerObserver) = NSignal<Int>.pipe()
signal.take(first: 2).observeValues { (value) in
print("received value: \(value)")
}
innerObserver.send(value: 1)
innerObserver.send(value: 2)
innerObserver.send(value: 3)
innerObserver.send(value: 4)
innerObserver.sendCompleted()
輸出: received value: 1
received value: 2
複製程式碼
take(first:): 只取最初N次的Event. 類似的還有signal.take(last: ): 只取最後N次的Event.
- merge
public static func merge(_ signals: Signal<Value, Error>...) -> Signal<Value, Error>
複製程式碼
let (signal1, innerObserver1) = NSignal<Int>.pipe()
let (signal2, innerObserver2) = NSignal<Int>.pipe()
let (signal3, innerObserver3) = NSignal<Int>.pipe()
Signal.merge(signal1, signal2, signal3).observeValues { (value) in
print("received value: \(value)")
}
innerObserver1.send(value: 1)
innerObserver1.sendCompleted()
innerObserver2.send(value: 2)
innerObserver2.sendCompleted()
innerObserver3.send(value: 3)
innerObserver3.sendCompleted()
輸出: received value: 1
received value: 2
received value: 3
複製程式碼
merge: 把多個訊號合併為一個新的訊號,任何一個訊號有Event的時就會這個新訊號就會Event傳送出來.
- combineLatest
public static func combineLatest<S: Sequence>(_ signals: S) -> Signal<[Value], Error>
複製程式碼
let (signal1, innerObserver1) = NSignal<Int>.pipe()
let (signal2, innerObserver2) = NSignal<Int>.pipe()
let (signal3, innerObserver3) = NSignal<Int>.pipe()
Signal.combineLatest(signal1, signal2, signal3).observeValues { (tuple) in
print("received value: \(tuple)")
}
innerObserver1.send(value: 1)
innerObserver2.send(value: 2)
innerObserver3.send(value: 3)
innerObserver1.send(value: 11)
innerObserver2.send(value: 22)
innerObserver3.send(value: 33)
innerObserver1.sendCompleted()
innerObserver2.sendCompleted()
innerObserver3.sendCompleted()
輸出: received value: (1, 2, 3)
received value: (11, 2, 3)
received value: (11, 22, 3)
received value: (11, 22, 33)
複製程式碼
combineLatest: 把多個訊號組合為一個新訊號,新訊號的Event是各個訊號的最新的Event的組合.
"組合"意味著每個訊號都至少有傳送過一次Event, 畢竟組合的每個部分都要有值. 所以, 如果有某個訊號一次都沒有傳送過Event, 那麼這個新訊號什麼也不會傳送, 不論其他訊號傳送了多少Event.
另外, 新訊號只會取最新的Event的來進行組合, 而不是數學意義上的組合.
- zip
public static func zip<S: Sequence>(_ signals: S) -> Signal<[Value], Error>
複製程式碼
let (signal1, innerObserver1) = NSignal<Int>.pipe()
let (signal2, innerObserver2) = NSignal<Int>.pipe()
let (signal3, innerObserver3) = NSignal<Int>.pipe()
Signal.zip(signal1, signal2, signal3).observeValues { (tuple) in
print("received value: \(tuple)")
}
innerObserver1.send(value: 1)
innerObserver2.send(value: 2)
innerObserver3.send(value: 3)
innerObserver1.send(value: 11)
innerObserver2.send(value: 22)
innerObserver3.send(value: 33)
innerObserver1.send(value: 111)
innerObserver2.send(value: 222)
innerObserver1.sendCompleted()
innerObserver2.sendCompleted()
innerObserver3.sendCompleted()
輸出: received value: (1, 2, 3)
received value: (11, 22, 33)
複製程式碼
zip: 新訊號的Event是各個訊號的最新的Event的進行拉鍊式組合.
有人把這個叫壓縮, 但我覺得拉鍊式組合更貼切一些. 拉鍊的左右齒必須對齊才能拉上, 這個函式也是一樣的道理. 只有各個訊號傳送Event的次數相同(對齊)時, 新訊號才會傳送組合值. 同理, 如果有訊號未傳送那麼什麼也不會發生.
我個人常用的Signal的相關函式就是這些, 其他的自己不怎麼用的函式就不介紹了, 接下來介紹下Signal的實現概述, 不關心的朋友請直接跳過.
- Signal的實現概述
讓我們點開Signal.swift, 你應該會看到Signal只有一個孤零零的core屬性一個設定core屬性的初始化函式以及一個呼叫core.observe的observe函式. 正如原始碼註釋中所說: Signal只是Core的一個殼. 所以接下來我們主要介紹的其實是Core. 我們來看看Core的主要定義:
private final class Core {
private let disposable: CompositeDisposable
private let stateLock: Lock //狀態鎖
private let sendLock: Lock //事件鎖
private var state: State //訊號狀態 最重要就是它了
...略
}
private enum State {
case alive(Bag<Observer>, hasDeinitialized: Bool)
case terminating(Bag<Observer>, TerminationKind)
case terminated
}
public struct Bag<Element> {//Bag可以認為是陣列的一層封裝
fileprivate var elements: ContiguousArray<Element> = []
...略
}
複製程式碼
Core裡面最重要的屬性就是這個State. State的作用有兩個: 一個是指示訊號的狀態, 另一個就是上文我提到過的儲存訊號訂閱者新增進來的Observer物件. 我們來看看新增Observer物件部分的程式碼:
fileprivate func observe(_ observer: Observer) -> Disposable? {
var token: Bag<Observer>.Token?
stateLock.lock()
//1. 訊號處於state.alive狀態 將新的observer物件新增到state.alive的陣列中
if case let .alive(observers, hasDeinitialized) = state {
var newObservers = observers
token = newObservers.insert(observer)
self.state = .alive(newObservers, hasDeinitialized: hasDeinitialized)
}
stateLock.unlock()
//2. 如果1順利執行 token會被賦值(即訊號處於alive狀態) 返回一個Disposable物件 否則直接向observer物件傳送interrupted事件
if let token = token {
return AnyDisposable { [weak self] in
self?.removeObserver(with: token)
}
} else {
observer.sendInterrupted()
return nil
}
}
複製程式碼
上面我說過Observer是Event的處理邏輯封裝, 這裡我們新增並儲存了Observer(也就是儲存了Event的處理邏輯), 接下來需要的就是在合適的時機執行這些Observer內部的處理邏輯. 這部分程式碼對應Core.send(_ event: Event):
private func send(_ event: Event) {
...lock部分程式碼 略
if event.isTerminating {
//1. 收到非Value的Event, 將訊號狀態從.alive切換到.terminating 並將.alive中的Observer陣列移到.terminating中(如果此時訊號處於.alive的話)
if case let .alive(observers, _) = state {
self.state = .terminating(observers, .init(event))
}
//2. 依次執行.terminating的陣列中Observer.send函式(如果有的話)
tryToCommitTermination()
} else {
//1. 收到Value的Event, 依次執行.alive的陣列中Observer.send函式(如果此時訊號處於.alive的話)
if case let .alive(observers, _) = self.state {
for observer in observers {
observer.send(event)
}
}
//2. 收到Value的Event 依次執行.terminating的陣列中Observer.send函式(如果此時訊號處於.terminating的話)
if case .terminating = state {
tryToCommitTermination()
}
}
}
private func tryToCommitTermination() {
...lock部分程式碼 略
if case let .terminating(observers, terminationKind) = state {
//1.切換狀態到.terminated
//2.依次執行.terminating的陣列中Observer.send函式(如果有的話)
state = .terminated
if let event = terminationKind.materialize() {
for observer in observers {
observer.send(event)
}
}
}
}
複製程式碼
我去掉了Core.send函式和lock相關的程式碼, 這樣看起來會簡單些, 大家對照註釋應該比較好理解, 這裡我主要說說State的切換. 先把State定義貼過來:
private enum State {
case alive(Bag<Observer>, hasDeinitialized: Bool)
case terminating(Bag<Observer>, TerminationKind)
case terminated
...一些函式 略
}
複製程式碼
可以看到第三個狀態terminated是不帶關聯陣列的, 這意味著當訊號切換到terminated狀態時, 那麼那些被儲存的Observer物件也就跟著釋放了, 所以, 當不再需要使用訊號時, 總是應該向訊號傳送一個非Value事件確保資源釋放.
事實上, 在ReactiveSwift5.0中Signal和State會保持一個引用環確保常駐記憶體, 如果你不傳送非Value事件的話, 資源是不會被釋放的. 但在最新的ReactiveSwift中, 這個引用環被去掉了, 你不用擔心資源釋放的問題. 但不再需要訊號的時候呼叫一下sendCompleted()總是一個好習慣.
現在我們知道了訊號狀態的切換, Observer的新增, Observer.send的執行, 那麼最後就只剩下將這一切連線起來了, 顯然, 這個連線函式就是Signal.pipe():
//Signal.pipe()
public static func pipe(disposable: Disposable? = nil) -> (output: Signal, input: Observer) {
var observer: Observer!
let signal = self.init { innerObserver, lifetime in
observer = innerObserver
lifetime += disposable
}
return (signal, observer)
}
//Signal.init()
public init(_ generator: (Observer, Lifetime) -> Void) {
core = Core(generator)
}
//Core.init()
fileprivate init(_ generator: (Observer, Lifetime) -> Void) {
//1. 設定訊號初始狀態為alive 同時初始化alive中的陣列
state = .alive(Bag(), hasDeinitialized: false)
...初始化lock 略
//2. 建立一個Observer物件並將該物件的_send閉包設定為Core.send函式
generator(Observer(action: self.send, interruptsOnDeinit: true), Lifetime(disposable))
}
複製程式碼
這裡我們看到, pipe()函式會通過一個generator: (Observer, Lifetime)閉包去建立Core物件, 然後通過這個Core物件去建立Signal, pipe()函式通過generator閉包捕獲了Core.init()中的innerObserver物件, 而這個InnerObserver物件的_send指向的其實是Core.send函式. 最後pipe()將建立完成Signal和InnerObserver打包返回.
這也是為什麼上文我說: pipe().output(即Signal)的作用是管理訊號狀態並儲存由訂閱者提供的Observer物件, 而pipe().input(即InnerObserver)的作用則是在接收到Event後依次執行這些被儲存的Observer._send.
我們把上面的流程用一張圖來表示:
這張圖其實也可用來標示OC的RACSubject或者RXSwift的RXSubject的大體工作流程, 畢竟三者都是State, 觀察者陣列, send函式, observer/subscribe函式, 只是加不加殼罷了.
SignalProducer
SignalProducer是ReactiveSwift中冷訊號的實現, 是第二種傳送事件的途徑.
上文說到熱訊號是活動著的事件發生器, 相對應的, 冷訊號則是休眠中的事件發生器. 也就是說冷訊號需要一個喚醒操作, 然後才能傳送事件, 而這個喚醒操作就是訂閱它. 因為訂閱後才傳送事件, 顯然, 冷訊號不存在時機早晚的問題.
仍以春晚舉例: 冷訊號相當於春晚的視訊檔案而不是現場直播, 正常情況下, 視訊檔案肯定是不會自動播放的, 但你只要一雙擊, 它就被啟動播放然後輸出節目了. 照例, 我還是給出兩份程式碼, 大家懂個意思就行:
//1. 通過SignalProducer.init(startHandler: (Observer, Lifetime) -> Void)建立SignalProducer
let producer = SignalProducer<Int, NoError> { (innerObserver, lifetime) in
lifetime.observeEnded({
print("訊號無效了 你可以在這裡進行一些清理工作")
})
//2. 向外界傳送事件
innerObserver.send(value: 1)
innerObserver.send(value: 2)
innerObserver.sendCompleted()
}
//3. 建立一個觀察者封裝事件處理邏輯
let outerObserver = Signal<Int, NoError>.Observer(value: { (value) in
print("did received value: \(value)")
})
//4. 新增觀察者到SignalProducer
producer.start(outerObserver)
輸出: did received value: 1
did received value: 2
訊號無效了 你可以在這裡進行一些清理工作
複製程式碼
typealias Producer<T> = ReactiveSwift.SignalProducer<T, NoError>
複製程式碼
let producer = Producer<Int> { (innerObserver, _) in
//沒什麼想清理的
innerObserver.send(value: 1)
innerObserver.send(value: 2)
innerObserver.sendCompleted()
}
producer.startWithValues { (value) in
print("did received value: \(value)")
}
producer.startWithFailed(action: )
producer.startWithResult(action: )
producer.startWithXXX...各種便利函式
複製程式碼
和Signal的訂閱方式如出一轍, 只是名字換了一下, Signal.observeXXX換成了SignalProducer.startXXX. 大家都是事件發生器, 所以API方面Signal和SignalProducer都是一樣的, 上面的map, on, merge, comblinelast...等等, SignalProducer也有一份, 作用也都一樣, 我就不多說了, 這裡簡單給兩段程式碼說說可能遇到的坑.
func fetchData(completionHandler: (Int, Error?) -> ()) {
print("發起網路請求")
completionHandler(1, nil)
}
let producer = Producer<Int> {[unowned self] (innerObserver, _) in
self.fetchData(completionHandler: { (data, error) in
innerObserver.send(value: data)
innerObserver.sendCompleted()
})
}
producer.startWithValues { (value) in
print("did received value: \(value)")
}
producer.startWithValues { (value) in
print("did received value: \(value)")
}
輸出: 發起網路請求
did received value: 1
發起網路請求
did received value: 1
複製程式碼
也許你只是想兩個觀察者共享一次網路請求帶回的Event, 但事實上這裡會發生兩次網路請求, 但這不是一個bug, 這是一個feature. SignalProducer的一個特性是, 每次被訂閱就會執行一次初始化時儲存的閉包. 所以如果你有類似一次執行, 多處訂閱的需求, 你應該選擇Signal而不是SignalProducer. 所以, 符合需求的程式碼可能是這樣:
let signalTuple = NSignal<Int>.pipe()
signalTuple.output.observeValues { (value) in
print("did received value: \(value)")
}
signalTuple.output.observeValues { (value) in
print("did received value: \(value)")
}
self.fetchData { (data, error) in
signalTuple.input.send(value: data)
signalTuple.input.sendCompleted()
}
輸出: 發起網路請求
did received value: 1
did received value: 1
複製程式碼
到目前為止, 示例程式碼中給到的都是NoError型別的訊號, 在實際開發中, 這顯然是不可能的, 畢竟錯誤是不可避免的. 通常我們的專案會宣告一個類似APIError的錯誤型別來表示這些錯誤, 所以你可能會有這樣的宣告:
struct APIError: Swift.Error {
let code: Int
var reason = ""
}
typealias NSignal<T> = ReactiveSwift.Signal<T, NoError>
typealias APISignal<T> = ReactiveSwift.Signal<T, APIError>
typealias Producer<T> = ReactiveSwift.SignalProducer<T, NoError>
typealias APIProducer<T> = ReactiveSwift.SignalProducer<T, APIError>
複製程式碼
這樣的宣告很好, 能讓ReactiveSwift寫起來像RXSwift一樣"簡潔". 但這裡需要加上下面的程式碼才能更好的工作:
extension SignalProducer where Error == APIError {
@discardableResult
func startWithValues(_ action: @escaping (Value) -> Void) -> Disposable {
return start(Signal.Observer(value: action))
}
}
複製程式碼
這是因為預設的SignalProducer是沒有startWithValues函式的, ReactiveSwift會在Extension裡給它加上startWithValues函式, 但是這隻對NoError有效, 所以當你在自定義Error時, 請記得加上類似的程式碼.
基本使用介紹完了, 照例是SignalProducer的實現概述, 不關心朋友的請直接跳過.
- SignalProducer的實現概述
和Signal類似, SignalProducer也是一個殼, 殼的內部裝著的是SignalProducerCore, 這個類有三個子類SignalCore, GeneratorCore和TransformerCore. 其中SignalCore和GeneratorCore用於普通操作的SignalProducer, 而TransformerCore則是在map, take, filterMap...之類的操作才會用上. 這裡我主要介紹使用率較高的SignalCore, 下面看看它的定義:
//SignalProducerCore
internal class SignalProducerCore {
//Instance作用:
//1.持有一個熱訊號Signal 用於儲存訂閱者新增的Observer物件
//2.持有一個() -> Void閉包 用於執行回撥(對子類SignalCore來說 這個閉包的作用則是向上面的Signal.core.state.Observes陣列傳送Event)
struct Instance {
let signal: Signal<Value, Error>
let observerDidSetup: () -> Void
let interruptHandle: Disposable
}
//抽象方法 留待子類實現
func makeInstance() -> Instance {
fatalError()
}
//抽象方法 留待子類實現(對子類SignalCore來說 這個函式就是訂閱Signal或者是新增Observer物件到Signal中)
func start(_ generator: (_ upstreamInterruptHandle: Disposable) -> Signal.Observer) -> Disposable {
fatalError()
}
...和TransformerCore相關的部分 略
}
//SignalCore
private final class SignalCore: SignalProducerCore {
private let _make: () -> Instance
//這個action會由SignalProducer傳入
init(_ action: @escaping () -> Instance) {
self._make = action
}
//當外部執行SignalProducer.start函式訂閱Producer時, 實際就是在執行這個函式
override func start(_ generator: (Disposable) -> Signal.Observer) -> Disposable {
let instance = makeInstance()// 1. 建立一個熱訊號signal
instance.signal.observe(generator(instance.interruptHandle)) 2. 通過引數generator建立一個觀察者並訂閱上面建立的signal
instance.observerDidSetup()3. 訂閱signal完成 執行回撥
return instance.interruptHandle
}
override func makeInstance() -> Instance {
return _make()
}
}
複製程式碼
現在我們知道了冷訊號是如何通過Signal來儲存訂閱者傳入的Observer物件, 下來看看這些Observer是如何被執行的:
//SignalProducer.swift
public init(_ startHandler: @escaping (Signal<Value, Error>.Observer, Lifetime) -> Void) {
self.init(SignalCore { //通過SignalCore.init(_ action:)建立Core Core.action就是makeInstance() 然後通過Core建立Producer
****SignalCore.makeInstance begin****
let disposable = CompositeDisposable()
//1. 建立一個Signal 這個Signal用於儲存訂閱者新增的Observer物件
let (signal, innerObserver) = Signal<Value, Error>.pipe(disposable: disposable)
//2.1 建立一個observerDidSetup 當訂閱Signal完成後就直接執行我們建立SignalProducer時傳入的startHandler
//2.2 innerObserver通過startHandler傳遞給外部使用 外部使用innerObserver傳送事件
let observerDidSetup = { startHandler(innerObserver, Lifetime(disposable)) }
let interruptHandle = AnyDisposable(observer.sendInterrupted)
//3. 通過Instance持有上面建立的Signal和observerDidSetup
return SignalProducerCore.Instance(signal: signal,
observerDidSetup: observerDidSetup,
interruptHandle: interruptHandle)
****SignalCore.makeInstance end****
})
}
}
//ViewModel.swift
let producer = Producer<Int> {[unowned self] (innerObserver, _) in
self.fetchData(completionHandler: { (data, error) in
innerObserver.send(value: data)//外界通過startHandler使用innerObserver傳送事件
innerObserver.sendCompleted()
})
}
複製程式碼
簡單描述一下整個流程:
- 建立Producer時需要傳入一個startHandler:(Signal.Observer, Lifetime) -> Void閉包, 我們通過這個startHandler的Observer引數傳送事件, startHandler會在Producer.core._make執行時被回撥.
- 那麼Producer.core._make閉包什麼時候會執行呢? 答案是一旦有人呼叫Producer.start(outerObserver)函式時. _make會建立一個Signal並訂閱outerObserver到Signal中, 然後將InnerObserver傳入startHandler閉包並執行.
記住, Producer.start呼叫幾次, Producer.core._make就會執行幾次(也就是startHandler會執行幾次). 照例, 給到一張圖:
這張圖不能用於OC或RXSwift, 這兩者的冷訊號並不依託熱訊號, 只是思路類似, 不過實現方式會略複雜些.Property/MutableProperty
ReactiveSwift傳送事件的第三種途徑是Property/MutableProperty. 從冷熱訊號的定義上來看, Property的行為應該屬於熱訊號, 但和上文的Signal不同, Property/MutableProperty只提供一種狀態的事件: Value.(雖然它有Completed狀態) 我們就暫且認為Property/MutableProperty代表那些不知道何時結束的現場直播吧. 照例, 先上一段標準程式碼:
let constant = Property(value: 1)
// constant.value = 2 //error: Property(value)建立的value不可變
print("initial value is: \(constant.value)")
constant.producer.startWithValues { (value) in
print("producer received: \(value)")
}
constant.signal.observeValues { (value) in
print("signal received: \(value)")
}
輸出: initial value is: 1
producer received: 1
複製程式碼
let mutableProperty = MutableProperty(1)
print("initial value is: \(mutableProperty.value)")
mutableProperty.producer.startWithValues { /** 冷訊號可以收到初始值value=1和2,3 */
print("producer received \($0)")
}
mutableProperty.signal.observeValues { /** 熱訊號只能收到後續的變化值value=2,3 */
print("signal received \($0)")
}
mutableProperty.value = 2 /** 設定value值就是在傳送Value事件 */
mutableProperty.value = 3 /** 設定value值就是在傳送Value事件 */
輸出: initial value is: 1
producer received: 1
producer received: 2
signal received: 2
producer received: 3
signal received: 3
複製程式碼
這段程式碼只是演示一下Property的基本資訊. 大家只需要知道Property.value不可設定, MutableProperty.value可設定. Property/MutableProperty內部有一個Producer一個Signal, 設定value即是在向這兩個訊號傳送Value事件即可.
下面以手機號輸入限制舉例給到一段實際使用的示例程式碼:
需求: 1.使用者輸入手機號 限制手機號最多輸入11個數字
2.驗證手機號是否有效 手機號無效需要展示錯誤資訊
let errorLabel: UILabel
let sendButton: UIButton
let phoneNumerTextField: UITextField
var errorText = MutableProperty("")
var validPhoneNumer = MutableProperty("")
errorLabel.reactive.text <~ errorText //繫結錯誤資訊到errorLabel
sendButton.reactive.isEnabled <~ errorText.map{ $0.count == 0 } //只是演示一下什麼都可以綁
sendButton.reactive.backgroundColor <~ errorText.map{ $0.count == 0 ? UIColor.red : UIColor.gray } //只是演示一下什麼都可以綁
phoneNumerTextField.reactive.text <~ validPhoneNumer //繫結有效輸入到輸入框
//有效輸入的資料來源來自原始的輸入框 我們對原始輸入進行一些格式化
validPhoneNumer <~ phoneNumerTextField.reactive.continuousTextValues
.map({(text) -> String in
let phoneNumer = (text ?? "").substring(to: 11) //1. 最多輸入11個數字, 多餘部分截掉
let isValidPhoneNum = NSPredicate(format: "SELF MATCHES %@", "正規表示式...").evaluate(with: phoneNumer) //2. 檢查手機格式是否正確
errorText.value = isValidPhoneNum ? "手機號格式不正確" : "" //2. 格式不正確顯示錯誤資訊
return phoneNumer //3. 返回擷取後的有效輸入
})
複製程式碼
上面的程式碼中出現了一個新東西: <~操作符. <~非常有用, 而且實現也非常的簡單, 我會在下文解釋, 這裡大家只需要知道: <~的左邊是繫結目標(BindingTargetProvider), 右邊則是資料來源(BindingSource), <~會把右邊資料來源的傳送出的Value直接繫結到左邊的目標上.
目前左側現成的繫結目標有Property/MutableProperty和一系列形如UIKit.reactive.xxx的擴充, 右側現成的資料來源則有Signal, SignalProducer以及Property/MutableProperty. 對於非現成的繫結目標, 我們可以參照現有擴充自行擴充, 非常簡單, 比如給YYLabel加個擴充:
//UILabel的預設擴充
extension Reactive where Base: UILabel {
public var text: BindingTarget<String?> {
return makeBindingTarget { $0.text = $1 }//$0表示UI控制元件本身 $1表示value
}
public var attributedText: BindingTarget<NSAttributedString?> {
return makeBindingTarget { $0.attributedText = $1 }
}
}
//照貓畫虎給YYLabel也加上擴充
extension Reactive where Base: YYLabel {
public var text: BindingTarget<String?> {
return makeBindingTarget { $0.text = $1 }//$0表示UI控制元件本身 $1表示value
}
public var attributedText: BindingTarget<NSAttributedString?> {
return makeBindingTarget { $0.attributedText = $1 }
}
}
複製程式碼
- <~的實現概述
Property的實現比較簡單, 大體流程就是內部存了一個Signal然後只轉發Signal的Value事件, 我就不多說了, 主要說說<~的實現(其實也非常簡單...).
//資料來源
public protocol BindingSource: SignalProducerConvertible {
associatedtype Value
associatedtype Error: Swift.Error
var producer: SignalProducer<Value, Error> { get }
}
extension Signal: BindingSource {}
extension SignalProducer: BindingSource {}
//繫結目標提供者
public protocol BindingTargetProvider {
associatedtype Value
var bindingTarget: BindingTarget<Value> { get }
}
//繫結目標
public struct BindingTarget<Value>: BindingTargetProvider {
public let lifetime: Lifetime //這裡定義何時取消訂閱資料來源
public let action: (Value) -> Void //這裡定義瞭如何利用資料來源傳送的Value(一般來說就是簡單的用Value設定某個屬性)
...其他程式碼 略
}
extension BindingTargetProvider {
@discardableResult
public static func <~ <Source: BindingSource>
(provider: Self, source: Source) -> Disposable?
where Source.Value == Value, Source.Error == NoError {
//訂閱右邊的資料來源提供的producer, 在訂閱回撥中執行繫結目標的action閉包
return source.producer
.take(during: provider.bindingTarget.lifetime)
.startWithValues(provider.bindingTarget.action)
}
}
複製程式碼
可以看到<~的實現非常簡單, 只有三行程式碼, 訂閱右邊的資料來源提供的producer, 在訂閱回撥中執行繫結目標的action閉包. 然後我們看看ReactiveSwift是如何給UI控制元件新增BindingTarget的, 以Label舉例:
public protocol ReactiveExtensionsProvider: class {}
extension ReactiveExtensionsProvider {
public var reactive: Reactive<Self> {
return Reactive(self)
}
public static var reactive: Reactive<Self>.Type {
return Reactive<Self>.self
}
}//任何物件獲取 someObject.reactive時就返回一個Reactive結構體, Reactive.base即是someObject
public struct Reactive<Base> {
public let base: Base
fileprivate init(_ base: Base) {
self.base = base
}
}
複製程式碼
extension Reactive where Base: NSObjectProtocol {
//建立一個BindingTarget BindingTarget.action則是在UI執行緒操作value
public func makeBindingTarget<U>(on scheduler: Scheduler = UIScheduler(), _ action: @escaping (Base, U) -> Void) -> BindingTarget<U> {
return BindingTarget(on: scheduler, lifetime: lifetime) { [weak base = self.base] value in
if let base = base {
action(base, value)
}
}
}
}
extension Reactive where Base: UILabel {
//當獲取label.reactive.text時 就建立一個makeBindingTarget makeBindingTarget的action則是直接設定self.text = value
public var text: BindingTarget<String?> {
return makeBindingTarget { $0.text = $1 }//$0即是label自己 $1即是value
}
}
複製程式碼
不到30行, 就不給圖了...
Action/CocoaAction
Action是最後一種傳送事件的途徑, 不過和其他途徑不同, 它並不直接傳送事件, 而是生產訊號, 由生產的訊號來傳送事件. 最重要的是, Action是唯一一種可以接受訂閱者輸入的途徑.
舉個栗子: 上文中的現場直播也好, 視訊檔案也好, 其實都是家長(ViewModel)提供的, 一般家長的特色就是隻是給予, 但並不在乎你喜不喜歡, 而且你還沒辦法通過訊號跟他進行溝通. 而Action不同, 它提供讓你跟家長溝通的介面apply(input), 當然, 雖然你說了你想說的(傳遞了資料), 但家長(ViewModel)採不採納就是另一回事了.
這裡直接上Action的實際使用程式碼:
//Action.swift
public final class Action<Input, Output, Error: Swift.Error>
public convenience init(execute: @escaping (Input) -> SignalProducer<Output, Error>)
複製程式碼
typealias APIAction<O> = ReactiveSwift.Action<[String: String]?, O, APIError>
1. 建立一個Action 輸入型別為[String: String]? 輸出型別為Int 錯誤型別為APIError
let action = APIAction<Int> { (input) -> APIProducer<Int> in
print("input: ", input)
return APIProducer({ (innerObserver, _) in
//發起網路請求
innerObserver.send(value: 1)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: {
innerObserver.send(value: 2)
innerObserver.sendCompleted()
})
})
}
2. 訂閱Action的執行事件
action.events.observe { print("did received Event: \($0)") }
3. 訂閱Action的各種輸出事件
action.values.observeValues { print("did received Value: \($0)") }
//action.errors.observeValues { print("did received Error: \($0)") }
//action.completed.observeValues { print("did received completed: \($0)") }
4. 執行Action 開始輸出
action.apply(["1": "xxx"]).start()
5. 在返回的Producer還未結束前繼續執行Action 什麼也不會輸出
for i in 0...10 {
action.apply([String(i): "xxx"]).start()
}
輸出: input: Optional(["0": "xxx"])
did received Value: 1
did received Event: VALUE VALUE 1
....兩秒後....
did received Value: 2
did received Event: VALUE VALUE 2
did received Event: VALUE COMPLETED
did received Event: COMPLETED
複製程式碼
Action的三個泛型從左到右依次定義了輸入型別, 輸出型別, 錯誤型別, 通常我都是直接typealias的, 不然寫起來真是太長了. 上面的程式碼主要描述以下資訊:
- 通過閉包execute: (Input) -> SignalProducer 建立一個Action, 我們可以通過execute.input獲取來自訂閱者的輸入, 然後我們需要返回一個SignalProducer, SignalProducer封裝了事件的獲取/傳送邏輯.
- 通過Action.events可以訂閱Action本身的事件, 此時的Event.Value還是一個Event.
- 通過Action.values/errors/completed可以訂閱初始化閉包中返回的SignalProducer的各種事件, 我們可以訂閱這些事件對返回的結果做相應的處理.
- 通過action.apply(input).start()提供輸入資訊並執行Action.
- 在返回的Producer還未結束前執行Action是沒用的, 只有上一個返回的Producer無效後, Action才能再次執行. (這個特性用來處理按鈕的多次點選傳送網路請求非常有用.)
我們看到第5條Action通過返回的Producer的狀態來控制自身的可執行與否, 大多數時候這就足夠了, 但保不齊你需要更精準的狀態控制, 此時你需要的是下面的函式:
public convenience init<P: PropertyProtocol>(enabledIf isEnabled: P, execute: @escaping (Input) -> SignalProducer<Output, Error>) where P.Value == Bool
複製程式碼
這個函式通過外部傳入的Property來控制Action的可執行與否, 我們可以用它來做較為精準的狀態控制. 舉個栗子:
需求: 只在輸入框有輸入時才可以點選按鈕發起網路請求
let executeButton: UIButton
//建立一個MutableProperty<Bool>的狀態控制訊號 控制邏輯隨便寫
let enabled = MutableProperty(false)
//有輸入才能發起請求
enabled <~ phoneNumberTF.reactive.continuousTextValues.map { return ($0 ?? "").count > 0 }
//傳入狀態控制訊號即可
let action = APIAction<Int>(enabledIf: enabled) { (input) -> APIProducer<Int> in
print("input: ", input)
return APIProducer({ (innerObserver, _) in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
innerObserver.send(value: 1)
innerObserver.sendCompleted()
})
})
}
action.values.observeValues { print("did received value: \($0)") }
executeButton.reactive.controlEvents(.touchUpInside).observeValues { (sender) in
action.apply(["xxx": "xxx"]).start()
}
複製程式碼
例子很簡單, 不多解釋, 最後介紹一下Action的Cocoa擴充: CocoaAction.
ReactiveCocoa為一些可點選的UI控制元件(如UIButton, UIBarButtonItem...)都新增了一個reactive.pressed屬性, 我們可以通過設定reactive.pressed很方便的新增點選操作, 不過這個屬性並不屬於Action而是CocoaAction. 下面簡單給到一段CocoaAction的用法:
typealias TextAction<O> = ReactiveSwift.Action<String?, O, APIError>
let executeButton: UIButton
let enabled = MutableProperty(false)
enabled <~ phoneNumberTF.reactive.continuousTextValues.map { return ($0 ?? "").count > 0 }
let action = TextAction<Int>(enabledIf: enabled) { (input) -> APIProducer<Int> in
print("input: ", input) //這裡的獲取到的都是self.phoneNumberTF.text
return APIProducer({ (innerObserver, _) in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
innerObserver.send(value: 1)
innerObserver.sendCompleted()
})
})
}
//通過CocoaAction給Button新增點選事件
executeButton.reactive.pressed = CocoaAction(action) {[unowned self] _ in
return self.phoneNumberTF.text //每次點選時都傳入此時的phoneNumberTF.text作為action的輸入
}
//初始化CocoaAction法1 input為一個固定值
let cocoaAction1 = CocoaAction<UIButton>(action, input: self.phoneNumberTF.text) //這樣寫 action得到的永遠都是""
//let cocoaAction1 = CocoaAction<UIButton>(action, input: nil)
//初始化CocoaAction法2 input為一個非固定值
let cocoaAction2 = CocoaAction<UIButton>(action) { _ in
let input = "xxx" //各種操作得到一個輸入
return input
}
複製程式碼
我們看到, 初始化CocoaAction需要兩個引數, action和input, action定義了點選控制元件時執行的操作, 而input則定義了操作執行時輸入的資料, input的型別需要合action.input的型別一一對應. 這裡只需要注意一點: 如果action的輸入是一個變化值, 比如來自某個輸入框textField, 那麼你應該通過閉包來提供這個輸入而不是直接傳入textField.text.
- Action的原始碼讀起來好累, 不寫...
ReactiveSwift的基本知識和用法, 到這裡就介紹完了. 最後我們把上面提到的東西全部串起來, 給到一個Hello ReactiveSwift.
Hello ReactiveSwift
我們的需求是一個很普通的註冊頁面:
鑑於這只是一個簡單的Demo, 我不會嚴格按照自己的開發模式來寫, 給到一個懶人版的MVVM即可, 首先是View的部分:
@IBOutlet weak var accountTF: UITextField! //賬號輸入框
@IBOutlet weak var passwordTF: UITextField! //密碼輸入框
@IBOutlet weak var ensurePasswordTF: UITextField! //確認密碼輸入框
@IBOutlet weak var verifyCodeTF: UITextField! //驗證碼輸入框
@IBOutlet weak var verifyCodeButton: UIButton! //獲取驗證碼按鈕
@IBOutlet weak var errorLabel: UILabel! //錯誤描述Label
@IBOutlet weak var submitButton: UIButton! //提交註冊按鈕
複製程式碼
這部分大家簡單瞄一眼即可, 記得大概屬性名即可, 接著我們定義一下ViewModel的介面部分, Protocol或Public皆可, 這裡我選擇前者:
protocol RegisterViewModelProtocol {
//設定資料來源依賴 (資料依賴是在初始化時注入還是使用前注入不是本文討論的話題 懶人版只選擇最方便的)
func setInput(accountInput: NSignal<String?>,
passwordInput: NSignal<String?>,
ensurePasswordInput: NSignal<String?>,
verifyCodeInput: NSignal<String?>)
var validAccount: MutableProperty<String> { get } //格式化好的賬號輸出
var validPassword: MutableProperty<String> { get } //格式化好的密碼輸出
var validEnsurePassword: MutableProperty<String> { get } //類似
var validVerifyCode: MutableProperty<String> { get } //類似
var errorText: MutableProperty<String> { get } //錯誤資訊輸出
var verifyCodeText: MutableProperty<String> { get } //驗證碼文案輸出
var getVerifyCodeAction: AnyAPIAction{ get } // 驗證碼按鈕點選事件
var submitAction: AnyAPIAction { get } //提交按鈕點選事件
}
複製程式碼
我們先來處理輸入格式的部分:
private let InvalidAccount = "手機號格式不正確"
private let InvalidPassword = "密碼格式不正確"
private let InvalidVerifyCode = "驗證碼格式不正確"
extension RegisterViewModel: RegisterViewModelProtocol{}
class RegisterViewModel {
private(set) var validAccount = MutableProperty("")
private(set) var validPassword = MutableProperty("")
private(set) var validEnsurePassword = MutableProperty("")
private(set) var validVerifyCode = MutableProperty("")
private var errors = (account: InvalidAccount, password: InvalidPassword, verifyCode: InvalidVerifyCode)
func setInput(accountInput: NSignal<String?>, passwordInput: NSignal<String?>, ensurePasswordInput: NSignal<String?>, verifyCodeInput: NSignal<String?>) {
//賬號: 11位手機號 最多輸入11個數字
validAccount <~ accountInput.map({[unowned self] (text) -> String in
let account = (text ?? "").substring(to: 11)
self.errors.account = !account.isValidPhoneNum ? InvalidAccount : ""
return account
})
//密碼: 6~16位數字和字元的組合 最多輸入16個字元
validPassword <~ passwordInput.map({[unowned self] (text) -> String in
let password = (text ?? "").substring(to: 16)
let isValidPassword = NSPredicate(format: "SELF MATCHES %@", "^(?=.*[a-zA-Z0-9].*)(?=.*[a-zA-Z\\W].*)(?=.*[0-9\\W].*).{6,16}$")
self.errors.password = !isValidPassword.evaluate(with: password) ? InvalidPassword : ""
return password
})
//確認密碼: 最多輸入16個字元 我們會在下文驗證兩次輸入是否一致
validEnsurePassword <~ ensurePasswordInput.map({
return ($0 ?? "").substring(to: 16)
})
//驗證碼: 1~6位數字或字元 最多輸入6個字元
validVerifyCode <~ verifyCodeInput.map({[unowned self] (text) -> String in
let verifyCode = (text ?? "").substring(to: 6)
let isValidVerifyCode = NSPredicate(format: "SELF MATCHES %@", "\\w+")
self.errors.verifyCode = !isValidVerifyCode.evaluate(with: verifyCode) ? InvalidVerifyCode : ""
return verifyCode
})
}
}
複製程式碼
有了有效輸入後, 我們就可以著手點選事件處理了, 先處理驗證碼的點選事件:
//因為沒後臺 所以給到一個任意輸出的訊號 只要不輸出Error 那就是請求成功
typealias AnyAPIAction = ReactiveSwift.Action<Any?, Any?, APIError>
typealias AnyAPIProducer = ReactiveSwift.SignalProducer<Any?, APIError>
1. 首先我們需要一個網路請求
class UserAPIManager: HTTPAPIManager {
//獲取驗證碼 (假裝有後臺)
func getVerifyCode(phoneNumber: String) -> AnyAPIProducer {
return arc4random() % 2 == 1 ? AnyAPIProducer(error: APIError(489489)) : AnyAPIProducer(value: true)
}
}
複製程式碼
class RegisterViewModel {
private var timer: Timer?
private var time = MutableProperty(60)
private(set) var errorText = MutableProperty("")
private(set) var verifyCodeText = MutableProperty("驗證碼")
2. 然後需要一個Action發起網路請求
private(set) lazy var getVerifyCodeAction = AnyAPIAction(enabledIf: **self.enableGetVerifyCode) { [unowned self] _ -> AnyAPIProducer in
return self.getVerifyCodeProducer
}
3. Action的執行條件是: 1)手機號格式正確 2)驗證碼倒數計時已結束或並未開始
private var enableGetVerifyCode: Property<Bool> {
return Property.combineLatest(time, errorText).map({ (time, error) -> Bool in
return error != InvalidAccount && (time <= 0 || time >= 60)
})
}
4. 發起驗證碼網路請求 請求成功後啟動timer進行60s倒數計時
private var getVerifyCodeProducer: AnyAPIProducer {
return UserAPIManager().getVerifyCode(phoneNumber: self.validAccount.value).on(value: { [unowned self] (value) in
self.timer?.invalidate()
self.time.value = 60;
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timeDown), userInfo: nil, repeats: true)
})
}
//5. 倒數計時 設定驗證碼按鈕文案
@objc private func timeDown() {
if (self.time.value > 0) {
self.verifyCodeText.value = String(self.time.value) + "s"
} else {
timer?.invalidate()
verifyCodeText.value = "驗證碼";
}
self.time.value -= 1;
}
}
複製程式碼
接著是提交註冊的點選事件:
1. 首先我們需要一個網路請求
class UserAPIManager: HTTPAPIManager {
func registerProducer(account: String, password: String, verifyCode: String) -> AnyAPIProducer {
//註冊使用者 (假裝有後臺)
return arc4random() % 2 == 1 ? AnyAPIProducer(error: APIError(789465)) : AnyAPIProducer(value: true)
}
}
複製程式碼
class RegisterViewModel {
private(set) var errorText = MutableProperty("")
2. 然後需要一個Action發起網路請求
private(set) lazy var submitAction: AnyAPIAction = AnyAPIAction(enabledIf: self.enableSubmit) { [unowned self] _ -> AnyAPIProducer in
return self.submitProducer
}
3. Action的執行條件是: 1)手機號格式正確 2)密碼格式正確 3)兩次輸入密碼一致 4)驗證碼格式正確
private var enableSubmit: Property<Bool> {
return Property.combineLatest(validAccount, validPassword, validEnsurePassword, validVerifyCode).map({ [unowned self] (account, password, ensurePassword, verifyCode) -> Bool in
//順便在這裡設定錯誤資訊
if self.errors.account.count > 0 {
self.errorText.value = self.errors.account
} else if self.errors.password.count > 0 {
self.errorText.value = self.errors.password
} else if password != ensurePassword {
self.errorText.value = "兩次輸入的密碼不一致"
} else if self.errors.verifyCode.count > 0 {
self.errorText.value = self.errors.verifyCode
} else {
self.errorText.value = ""
}
return self.errorText.value.count == 0
})
}
4.發起提交註冊網路請求 請求成功後儲存一些使用者資訊
private var submitProducer: AnyAPIProducer {
return UserAPIManager().registerProducer(account: validAccount.value, password: self.validPassword.value, verifyCode: validVerifyCode.code).on(value: { [unowned self] (value) in
self.timer?.invalidate()
UserDefaults.account = value
})
}
}
複製程式碼
ViewModel的介面都實現了, 接下來就是Controller來使用這些介面對View進行繫結:
class RegisterViewController: UIViewController {
private var viewModel: RegisterViewModelProtocol! {
didSet {
//1. 設定依賴資料來源
viewModel.setInput(accountInput: accountTF.reactive.continuousTextValues,
passwordInput: passwordTF.reactive.continuousTextValues,
ensurePasswordInput: ensurePasswordTF.reactive.continuousTextValues,
verifyCodeInput: verifyCodeTF.reactive.continuousTextValues)
//2. 繫結有效輸入
accountTF.reactive.text <~ viewModel.validAccount
passwordTF.reactive.text <~ viewModel.validPassword
ensurePasswordTF.reactive.text <~ viewModel.validEnsurePassword
//3. 繫結錯誤資訊
errorLabel.reactive.text <~ viewModel.errorText.signal.skip(first: 1)
//4. 繫結驗證碼相關資訊
verifyCodeTF.reactive.text <~ viewModel.validVerifyCode
verifyCodeButton.reactive.title <~ viewModel.verifyCodeText
//5. 繫結驗證碼點選事件(因為前面已經注入了驗證碼輸入 所以這裡我們不需要給到input)
verifyCodeButton.reactive.pressed = CocoaAction(viewModel.getVerifyCodeAction)
viewModel.getVerifyCodeAction.errors.observeValues {[unowned self] (error) in
self.view.toast(error.reason)//驗證碼獲取失敗了 禮貌性的給個toast
}
//6. 繫結提交註冊點選事件(同樣的 不需要給到input)
submitButton.reactive.pressed = CocoaAction(viewModel.submitAction)
viewModel.submitAction.errors.observeValues {[unowned self] (error) in
self.view.toast(error.reason)//註冊失敗了 禮貌性的給個toast
}
viewModel.submitAction.values.observeValues {[unowned self] (value) in
//註冊成功返回首頁 至於資訊儲存一類的事情ViewModel已經做完了 Controller做好UI展示就夠了
self.view.toast("註冊成功")
self.navigationController?.popViewController(animated: true)
}
}
}
}
複製程式碼
事實上, 在一個函式寫完所有的繫結邏輯並不是什麼好的程式碼規範(我應該把它拆開成多個小函式), 但考慮到這部分程式碼不多且只是個demo, 就先湊合吧. 荊軻刺秦王...
也許你會用得上這些
ReactiveSwift的更多API示例程式碼請下載ReactiveSwift. 下載下來後: 1) 開啟ReactiveSwift.xcworkspace 2)切換到 Result-Mac scheme進行編譯 3)然後切換到 ReactiveSwift-macOS scheme進行編譯 4)跟著ReactiveSwift.playground敲吧