
这篇文章来总结下Action。
什么是Action?
An action is defined as a unit of repeatable work which gets executed with a varying input.
Action表示可以稍后开始的重复的任务,类似于SignalProducer,有以下特点:
- 串行执行。
- 能够可变输入。
- 有条件地启用执行。
- 能够检查执行是否处在进行中。
Action怎么用?
当然,还是通过例子来展示用法。
每隔N秒打印一条消息。
1. 先用SignalProducer来实现
我们知道,如果我们想要推延Signal,在之后我们需要的某个时期再开始执行,我们可以使用SignalProducer
来封装任务。当时,我们创建了一个SignalProducer
,它在接下来的50秒内每5秒发出一个整数。但是,当我们想在每次启动SignalProducer
时提供不同的时间间隔时,我们会怎么做呢?
第一步,创建一个(Int) -> SignalProducer闭包:
// Returns a SignalProducer which emits interger after given seconds
let signalProducerGenerator: (Int) -> SignalProducer<Int, NoError> = { timeInterval in
return SignalProducer<Int, NoError> { (observer, lifetime) in
let now = DispatchTime.now()
for index in 0..<10 {
let timeElapsed = index * timeInterval
DispatchQueue.main.asyncAfter(deadline: now + Double(timeElapsed)) {
guard !lifetime.hasEnded else {
observer.sendInterrupted()
return
}
observer.send(value: timeElapsed)
if index == 9 {
observer.sendCompleted()
}
}
}
}
}
第二步,执行闭包,创建signalProducer:
let signalProducer1 = signalProducerGenerator(1)
let signalProducer2 = signalProducerGenerator(2)
第三步,启动任务
signalProducer1.startWithValues { value in
print("value from signalProducer1 = \(value)")
}
signalProducer2.startWithValues { value in
print("value from signalProducer2 = \(value)")
}
看起来不错,是吧?
现在我想让这两个任务一先一后执行,也就是串行,怎么办呢?
这时我就可以用Action来做:
Action的核心是个闭包,它以SignalProducer
的形式封装了可重复的任务。当我们调用apply()
时,闭包就会被执行,从而创建一个SignalProducer
。然后,调用SignalProducer
的start
方法。而且在执行过程中,除非当前任务执行完成,否则无法启动新的任务,如下图所示:

Action 有以下几个重要的属性:
- values: 由Action的所有调用生成的所有值的信号。
- error: 由Action的所有调用生成的所有错误的信号。
- isExecuting: Property<Bool>类型的属性,表示操作当前是否正在执行。
- isEnabled: Property<Bool>类型的属性,表示操作当前是否被启用。
Action是一个泛型类,它接受三个泛型参数:
//Action.swift
public final class Action<Input, Output, Error: Swift.Error>
public convenience init(execute: @escaping (Input) -> SignalProducer<Output, Error>)
- Input: 表示由apply()提供的外部输入类型。
- Output: 表示Action的信号发出的值的类型。
- Error: 表示信号发送的错误类型。
2. 上面的例子我们用Action来实现一下
我们需要Int类型的输入、Int类型的输出和NoError类型的错误。
同时需要一个(Input) -> SignalProducer<Output, Error>类型的闭包来创建Action
第一步,使用上面定义好的signalProducerGenerator闭包创建Action:
let action = Action<(Int), Int, NoError>(execute: signalProducerGenerator)
第二步,为Action的信号定义观察者:
//1. Observe values received
action.values.observeValues { value in
print("Time elapsed = \(value)")
}
//2. Observe when action completes
action.values.observeCompleted {
print("Action completed")
}
第三步,传入不同输入值:
//1. Apply the action with inputs and start it
action.apply(1).start()
//2. Ignored as action was busy executing
action.apply(2).start()
//3. Will be executed as it is started after `action.apply(1)` completed
DispatchQueue.main.asyncAfter(deadline: .now() + 12.0) {
action.apply(3).start()
}
- 当执行第一个apply后,日志每隔1秒打印一次。
- 由于第一个apply语句在执行,第二个apply将会被忽略。
- 第三个apply在12秒后执行,因为此时第一个apply已经执行完毕。在这种情况下,日志每3秒打印一次。
完整代码如下 :
// Define an action with a closure
let action = Action<(Int), Int, NoError>(execute: getSignalProducer)
// Observe values received
action.values.observeValues { value in
print("Time elapsed = \(value)")
}
// Observe when action completes
action.completed.observeValues {
print("Action completed")
}
// Apply the action with inputs and start it
action.apply(1).start()
action.apply(2).start() //Ignored as action was busy executing
DispatchQueue.main.asyncAfter(deadline: .now() + 12.0) {
//Will be executed as it is started after `action.apply(1)` completed
action.apply(3).start()
}
我们来回顾上面的内容:
- 我们定义了一个闭包,它返回一个SignalProducer。
- 用这个闭包创建一个Action。
- 观察这个Action。
- apply传入参数,start开启任务执行。
在上面的示例中,我们定义了一个仅依赖于通过apply方法提供的外部输入的操作。不过,还可以对操作建模。
建模创建Action
还是举个例子:
要求文本输入至少有10个字符。我们需要设计一个验证器,它将接受最小字符长度,并对当前文本执行长度检查。
第一步,定义一个闭包,第一个参数表示当前状态,第二个表示apply()方法传入的参数:
(State.Value, Input) -> SignalProducer<Output, Error>
在我们的例子中,第一个参数是文字输入,第二个是最小字符长度,是apply传入的参数。
所以我们可以定义如下闭包:
func lengthCheckerSignalProducer(text: String, minimumLength: Int) -> SignalProducer<Bool, NoError> {
return SignalProducer<Bool, NoError> { (observer, _) in
observer.send(value: (text.count > minimumLength))
observer.sendCompleted()
}
}
第二步,创建一个信号:
我们通过一个信号来模拟文本输入,该信号每秒将text中一个子串发出:
func textSignalGenerator(text: String) -> Signal<String, NoError> {
return Signal<String, NoError> { (observer, _) in
let now = DispatchTime.now()
for index in 0..<text.count {
DispatchQueue.main.asyncAfter(deadline: now + 1.0 * Double(index)) {
let indexStartOfText = text.index(text.startIndex, offsetBy: 0)
let indexEndOfText = text.index(text.startIndex, offsetBy: index)
let substring = text[indexStartOfText...indexEndOfText]
let value = String(substring)
observer.send(value: value)
}
}
}
}
第三步,创建Property:
let title = "ReactiveSwift"
let titleSignal = textSignalGenerator(text: title)
let titleProperty = Property(initial: "", then: titleSignal)
对于本例,我们需要将用户的输入转成一个信号,然后创建一个Property,从该信号中获取值。
第四步,创建Action :
let titleLengthChecker = Action<Int, Bool, NoError>(
state: titleProperty,
execute: lengthCheckerSignalProducer
)
第五步,观察:
Action的values属性提供了Signal,我们创建一个Observer来观察Action计算的结果:
titleLengthChecker.values.observeValues { isValid in
print("is title valid = \(isValid)")
}
第六步,调用apply和start方法,执行Action:
for i in 0..<title.count {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(i)) {
titleLengthChecker.apply(10).start()
}
}
最终输出:
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = false
is title valid = true
is title valid = true
is title valid = true
很明显,text的子串中只有3个的长度超过10的。
我们还可以有条件地启用Action。假设我们想只在字符数大于5时才执行。我们可以这样定义:
let titleLengthChecker = Action<Int, Bool, NoError>(
state: titleProperty,
enabledIf: { $0.count > 5 },
execute: lengthCheckerSignalProducer
)
Action在网络请求时很有用,将网络请求封装到Action 中的好处是,多次请求之间是有排他性的,也就是说,如果我们多次触发同一个请求,那么在上次请求结束前,下次的请求会被忽略,另外可以有条件的启动请求,取消请求。
CocoaAction
CocoaAction是Action的Cocoa拓展,ReactiveCocoa为一些可点击的UI控件(如UIButton, UIBarButtonItem...)都添加了一个reactive.pressed属性, 我们可以通过设置reactive.pressed很方便的添加点击操作, 不过这个属性并不属于Action而是CocoaAction。
还是用例子来认识它。
需求: 只在输入框有输入时才可以点击按钮发起网络请求
let executeButton: UIButton
let phoneNumberTF:UITextField
第一步,创建一个MutableProperty<Bool>的属性
let enabled = MutableProperty(false)
enabled <~ phoneNumberTF.reactive.continuousTextValues.map { return ($0 ?? "").count > 0 }
第二步,创建Action
typealias TextAction<T> = Action<String?, T, APIError>
typealias APIProducer <T> = SignalProducer<T, NoError>
let action = TextAction<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值变化
action.values.observeValues { print("did received value: \($0)") }
/*
executeButton.reactive.controlEvents(.touchUpInside).observeValues { (sender) in
action.apply(["xxx": "xxx"]).start()
}
*/
第四步,Button添加点击事件
//通过CocoaAction给Button添加点击事件
executeButton.reactive.pressed = CocoaAction(action) {[unowned self] _ in
return self.phoneNumberTF.text
}
每次点击时都传入此时的phoneNumberTF.text作为action的输入。
初始化CocoaAction有三种:
- 方法一:
let cocoaAction1 = CocoaAction<UIButton>(action, input: self.phoneNumberTF.text) //这种输入的value一直是""
let cocoaAction2 = CocoaAction<UIButton>(action, input: nil)
- 方法二:
let cocoaAction = CocoaAction<UIButton>(action) { _ in
return self.phoneNumberTF.text
}
- 方法三:
let cocoaAction = CocoaAction<UIButton>(action)
我们看到, 初始化CocoaAction需要两个参数:action和input。
- action定义了点击控件时执行的操作。
- input则定义了操作执行时输入的数据。
- input的类型需要跟action.input的类型一一对应。
Notes: 如果action的输入是一个变化值, 比如来自某个输入框textField, 那么你应该通过闭包来提供这个输入而不是直接传入textField.text,否则这种输入的value一直是""。
网友评论