【问题标题】:How to implement a basic UITextField input + UIButton action scenario using ReactiveCocoa 3?如何使用 ReactiveCocoa 3 实现基本的 UITextField 输入 + UIButton 动作场景?
【发布时间】:2015-07-19 19:43:37
【问题描述】:

我同时是 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 { $0! as! String }
        |> catch {_ in SignalProducer(value: "") }
}

使用此设置,当视图模型的文本长度超过 3 个字符时,按钮会启用。当用户点击按钮时,视图模型的动作就会运行,我可以将返回值设为 true。到目前为止一切顺利。

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

【问题讨论】:

  • 我觉得这个问题更适合codereview.stackexchange.com
  • 好的,谢谢你的建议。我将编辑我的帖子,删除问题 #1 并将 #2 作为唯一问题。 @JakubVano
  • @davsan 请注意,由于 buttonEnabled 绑定到 UI 属性,您应该在主线程上观察它,例如buttonEnabled &lt;~ viewModel.validatedTextProducer |&gt; observeOn(UIScheduler())
  • 好的,感谢您的提醒! @memmons

标签: ios swift mvvm reactive-cocoa reactive-cocoa-3


【解决方案1】:

如果你看看 Colin Eberhardt blog post on 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 = $0 }, { self.alpha  })
    }

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

extension UIBarItem {
    public var rac_enabled: MutableProperty<Bool> {
        return lazyMutableProperty(self, &AssociationKey.enabled, { self.enabled = $0 }, { 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 $0.0 != nil && $0.1 != nil }

这一切都来自他的博客文章,比这个答案更具描述性。我强烈建议任何对使用 RAC 3 感兴趣的人阅读他的精彩文章和教程:

【讨论】:

  • 是的,当我发布我的问题时,Colin 的最后一篇博文还不存在,所以我试图找到自己的做事方式。博文一发表我就已经阅读了,但仍然感谢您提及它!
【解决方案2】:

来自ReactiveCocoa/CHANGELOG.md

动作必须指明它接受的输入类型、产生的输出类型以及可能发生的错误类型(如果有的话)。

所以目前没有办法在没有输入的情况下定义Action

我想你可以声明你不关心输入,方法是把它设为 AnyObject? 并使用方便的初始化程序创建 CocoaAction

cocoaAction = CocoaAction(viewModel.action)

补充说明

  • 我不喜欢用AnyObject? 代替Bool 代替validatedTextProducer。我想您更喜欢它,因为绑定到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 { $0 as AnyObject }
        //...
    }
    

【讨论】:

  • 这些对我来说也很有意义。感谢您的所有建议!
猜你喜欢
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-02
  • 2019-06-04
  • 2015-07-26
  • 1970-01-01
相关资源
最近更新 更多