¿Cómo implementar un escenario básico de input UITextField + UIButton usando ReactiveCocoa 3?

Soy un noob Swift y ReactiveCocoa al mismo time. Utilizando el marco MVVM y Reactivo Cocoa v3.0-beta.4, me gustaría implementar esta configuration para aprender los conceptos básicos del nuevo marco RAC 3.

Tengo un campo de text y quiero que la input de text contenga más de 3 letras para la validation. Si el text pasa la validation, el button debajo debe estar habilitado. Cuando el button recibe el evento táctil, deseo activar una acción usando la propiedad del model de vista.

Dado que actualmente hay muy pocos resources sobre RAC 3.0 beta, implementé lo siguiente al leer los QA en el repository Github del marco. Esto es lo que podría llegar hasta ahora:

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: "") } } 

Con esta configuration, el button se habilita cuando el text del model de vista tiene más de 3 caracteres. Cuando el usuario toca el button, la acción del model de vista se ejecuta y puedo get el valor de retorno como verdadero. Hasta aquí todo bien.

Mi pregunta es: Dentro de la acción del model de vista, quiero usar su propiedad de text almacenada y actualizar el código para hacer una request de networking que lo use. Por lo tanto, no necesito una input desde el lado del controller de vista. ¿Cómo no puedo solicitar una input para mi propiedad Action?

    Desde el ReactiveCocoa / CHANGELOG.md :

    Una acción debe indicar el tipo de input que acepta, el tipo de salida que produce y qué types de errores pueden ocurrir (si existen).

    Entonces, actualmente no hay forma de definir una Action sin una input.

    Supongo que podrías declarar que no te importa la input al convertirlo en AnyObject? y creando CocoaAction con CocoaAction de conveniencia:

     cocoaAction = CocoaAction(viewModel.action) 

    Observaciones adicionales

    • No me gusta usar AnyObject? en lugar de Bool para validatedTextProducer . Supongo que lo buttonEnabled porque el enlace a la propiedad AnyObject? requiere AnyObject? . Preferiría lanzarlo allí, sin embargo, en lugar de sacrificar la claridad de tipo de mi model de vista (ver el ejemplo a continuación).

    • Es posible que desee restringir la ejecución de la Action en el nivel del model de vista, así como en la interfaz de usuario, por ejemplo:

       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 //... } } } 

      Y enlace a buttonEnabled :

       func bindSignals() { buttonEnabled <~ viewModel.action.enabled.producer |> map { $0 as AnyObject } //... } 

    Si echas un vistazo a la publicación del blog Colin Eberhardt en ReactiveCocoa 3, hay un enfoque muy bueno para este problema.

    Básicamente porque todavía está en versión beta, no existe una extensión en UIView que haga que esas properties sean fáciles de usar con RAC3, pero puede agregarlas fácilmente. Recomendaría agregar una extensión UIKit+RAC3.swift y agregarlas según lo necesite:

     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 }) } } 

    De esta forma, simplemente reemplaza la lógica RAC = RACObserve por (por ejemplo):

     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 } 

    De nuevo, todo esto se toma de su publicación de blog, que es mucho más descriptiva que esta respuesta. Recomiendo encarecidamente a cualquiera que esté interesado en usar RAC 3, lee sus increíbles publicaciones y tutoriales:

    • Una primera mirada al RAC 3
    • Productores de señal y claridad de API
    • MVVM y RAC 3