如何使用 ReactiveCocoa 3 实现基本的 UITextField 输入 + UIButton 操作场景?

How to implement a basic UITextField input + UIButton action scenario using ReactiveCocoa 3?

我同时是 Swift 和 ReactiveCocoa 菜鸟。使用 MVVM 和 Reactive Cocoa v3.0-beta.4 框架,我想实现这个设置,以学习新 RAC 3 框架的基础知识。

我有一个文本字段,我希望文本输入包含 3 个以上的字母,以进行验证。如果文本通过验证,下面的按钮应该被启用。当按钮接收到触摸事件时,我想使用视图模型的 属性.

触发一个动作

由于目前关于 RAC 3.0 beta 的资源非常少,我通过阅读框架 Github 回购上的 QA 实现了以下内容。到目前为止,这是我能想到的:

ViewModel.swift

class ViewModel {

    var text = MutableProperty<String>("")
    let action: Action<String, Bool, NoError>
    let validatedTextProducer: SignalProducer<AnyObject?, NoError>

    init() {
        let validation: Signal<String, NoError> -> Signal<AnyObject?, NoError> = map ({
            string in
            return (count(string) > 3) as AnyObject?
        })

        validatedTextProducer = text.producer.lift(validation)

        //Dummy action for now. Will make a network request using the text property in the real app. 
        action = Action { _ in
            return SignalProducer { sink, disposable in
                sendNext(sink, true)
                sendCompleted(sink)
            }
        }
    }
}

ViewController.swift

class ViewController: UIViewController {

    private lazy var txtField: UITextField = {
        return createTextFieldAsSubviewOfView(self.view)
    }()

    private lazy var button: UIButton = {
        return createButtonAsSubviewOfView(self.view)
    }()

    private lazy var buttonEnabled: DynamicProperty = {
       return DynamicProperty(object: self.button, keyPath: "enabled")
    }()

    private let viewModel = ViewModel()
    private var cocoaAction: CocoaAction?

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

        bindSignals()
    }

    func bindSignals() {
        viewModel.text <~ textSignal(txtField)
        buttonEnabled <~ viewModel.validatedTextProducer

        cocoaAction = CocoaAction(viewModel.action, input:"Actually I don't need any input.")
        button.addTarget(cocoaAction, action: CocoaAction.selector, forControlEvents: UIControlEvents.TouchDown)

        viewModel.action.values.observe(next: {value in
            println("view model action result \(value)")
        })
    }

    override func updateViewConstraints() {
        super.updateViewConstraints()

        //Some autolayout code here
    }
}

RACUtilities.swift

func textSignal(textField: UITextField) -> SignalProducer<String, NoError> {
    return textField.rac_textSignal().toSignalProducer()
        |> map { [=12=]! as! String }
        |> catch {_ in SignalProducer(value: "") }
}

使用此设置,当视图模型的文本超过 3 个字符时,该按钮将被启用。当用户点击按钮时,视图模型的操作运行,我可以获得 return 值为 true。到目前为止一切顺利。

我的问题是:在视图模型的操作中,我想使用其存储的文本 属性 并更新代码以使用它发出网络请求。所以,我不需要来自视图控制器端的输入。我如何才能要求我的操作属性输入?

来自ReactiveCocoa/CHANGELOG.md

An action must indicate the type of input it accepts, the type of output it produces, and what kinds of errors can occur (if any).

因此,目前无法在没有输入的情况下定义 Action

我想你可以通过 AnyObject? 并使用便利初始化器创建 CocoaAction 来声明你不关心输入:

cocoaAction = CocoaAction(viewModel.action)

补充说明

  • 我不喜欢在 validatedTextProducer 中使用 AnyObject? 而不是 Bool。我想您更喜欢它,因为绑定到 buttonEnabled 属性 需要 AnyObject?。我宁愿把它放在那里,而不是牺牲我的视图模型的类型清晰度(见下面的例子)。

  • 您可能希望在视图模型级别以及 UI 上限制执行 Action,例如:

    class ViewModel {
    
        var text = MutableProperty<String>("")
        let action: Action<AnyObject?, Bool, NoError>
    
        // if you want to provide outside access to the property
        var textValid: PropertyOf<Bool> {
            return PropertyOf(_textValid)
        }
    
        private let _textValid = MutableProperty(false)
    
        init() {
            let validation: Signal<String, NoError> -> Signal<Bool, NoError> = map { string in
                return count(string) > 3
            }
    
            _textValid <~ text.producer |> validation
    
            action = Action(enabledIf:_textValid) { _ in
                //...
            }
        }
    }
    

    并绑定到 buttonEnabled:

    func bindSignals() {
        buttonEnabled <~ viewModel.action.enabled.producer |> map { [=12=] as AnyObject }
        //...
    }
    

如果你看一下 Colin Eberhardt blog post 关于 ReactiveCocoa 3 的文章,有一个很好的方法来解决这个问题。

基本上,因为它仍处于测试阶段,所以 UIView 上没有使这些属性易于与 RAC3 一起使用的扩展,但您可以轻松添加它们。我建议添加一个 UIKit+RAC3.swift 扩展并根据需要添加它们:

import UIKit
import ReactiveCocoa

struct AssociationKey {
    static var hidden: UInt8 = 1
    static var alpha: UInt8 = 2
    static var text: UInt8 = 3
    static var enabled: UInt8 = 4
}

func lazyAssociatedProperty<T: AnyObject>(host: AnyObject,
    key: UnsafePointer<Void>, factory: ()->T) -> T {
        var associatedProperty = objc_getAssociatedObject(host, key) as? T

        if associatedProperty == nil {
            associatedProperty = factory()
            objc_setAssociatedObject(host, key, associatedProperty,
                UInt(OBJC_ASSOCIATION_RETAIN))
        }
        return associatedProperty!
}

func lazyMutableProperty<T>(host: AnyObject, key: UnsafePointer<Void>,
    setter: T -> (), getter: () -> T) -> MutableProperty<T> {
        return lazyAssociatedProperty(host, key) {
            var property = MutableProperty<T>(getter())
            property.producer
                .start(next: {
                    newValue in
                    setter(newValue)
                })
            return property
        }
}

extension UIView {
    public var rac_alpha: MutableProperty<CGFloat> {
        return lazyMutableProperty(self, &AssociationKey.alpha, { self.alpha = [=10=] }, { self.alpha  })
    }

    public var rac_hidden: MutableProperty<Bool> {
        return lazyMutableProperty(self, &AssociationKey.hidden, { self.hidden = [=10=] }, { self.hidden  })
    }
}

extension UIBarItem {
    public var rac_enabled: MutableProperty<Bool> {
        return lazyMutableProperty(self, &AssociationKey.enabled, { self.enabled = [=10=] }, { self.enabled  })
    }
}

这样您只需将 RAC = RACObserve 逻辑替换为(例如):

var date = MutableProperty<NSDate?>(nil)
var time = MutableProperty<Int?>(nil)

let doneItem = UIBarButtonItem()
doneItem.rac_enabled <~ date.producer
        |> combineLatestWith(time.producer)
        |> map { return [=11=].0 != nil && [=11=].1 != nil }

同样,这全部取自他的博客 post,这比这个答案更具描述性。我强烈建议任何对使用 RAC 3 感兴趣的人阅读他的精彩 posts 和教程: