美文网首页
ReactiveSwift框架分析5 — Action和Coco

ReactiveSwift框架分析5 — Action和Coco

作者: 沈枫_ShenF | 来源:发表于2019-06-06 22:32 被阅读0次

这篇文章来总结下Action。

什么是Action?

An action is defined as a unit of repeatable work which gets executed with a varying input.

Action表示可以稍后开始的重复的任务,类似于SignalProducer,有以下特点:

  1. 串行执行。
  2. 能够可变输入。
  3. 有条件地启用执行。
  4. 能够检查执行是否处在进行中。

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。然后,调用SignalProducerstart方法。而且在执行过程中,除非当前任务执行完成,否则无法启动新的任务,如下图所示:

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()
}
  1. 当执行第一个apply后,日志每隔1秒打印一次。
  2. 由于第一个apply语句在执行,第二个apply将会被忽略。
  3. 第三个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()
    }

我们来回顾上面的内容:

  1. 我们定义了一个闭包,它返回一个SignalProducer。
  2. 用这个闭包创建一个Action。
  3. 观察这个Action。
  4. 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有三种:

  1. 方法一:
let cocoaAction1 = CocoaAction<UIButton>(action, input: self.phoneNumberTF.text) //这种输入的value一直是""
let cocoaAction2 = CocoaAction<UIButton>(action, input: nil)
  1. 方法二:
let cocoaAction = CocoaAction<UIButton>(action) { _ in
    return self.phoneNumberTF.text 
}
  1. 方法三:
let cocoaAction = CocoaAction<UIButton>(action) 

我们看到, 初始化CocoaAction需要两个参数:action和input。

  • action定义了点击控件时执行的操作。
  • input则定义了操作执行时输入的数据。
  • input的类型需要跟action.input的类型一一对应。

Notes: 如果action的输入是一个变化值, 比如来自某个输入框textField, 那么你应该通过闭包来提供这个输入而不是直接传入textField.text,否则这种输入的value一直是""。

相关文章

网友评论

      本文标题:ReactiveSwift框架分析5 — Action和Coco

      本文链接:https://www.haomeiwen.com/subject/pgmuxctx.html