如何使用 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。到目前为止一切顺利。
我的问题是:在视图模型的操作中,我想使用其存储的文本 属性 并更新代码以使用它发出网络请求。所以,我不需要来自视图控制器端的输入。我如何才能不要求我的操作属性输入?
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 和教程:
我同时是 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。到目前为止一切顺利。
我的问题是:在视图模型的操作中,我想使用其存储的文本 属性 并更新代码以使用它发出网络请求。所以,我不需要来自视图控制器端的输入。我如何才能不要求我的操作属性输入?
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 和教程: