ReactiveSwift + MVVM 登录demo详解
最近在研究Swift下的MVVM模式,就尝试用ReactiveCocoa来便捷绑定VM,怎么知道和ReactiveObjc的API相差甚远,原来API在5.0后就大改了,目前中文资料又不多,只能自己在Github和Stackoverflow上看老外的提问,同时也感谢EvilNOP的文章:零基础上手ReactiveSwift & ReactiveCocoa教程!
VM
先上代码:
class LoginViewModel: NSObject {
var userNameSignal : Signal<String?, NoError>//接收用户名的信号
var passWordSignal : Signal<String?, NoError>//接收密码的信号
var validSignal : Signal<Bool, NoError>//用户名和密码是否合法的信号
var loginAction : Action<(String, String), Bool, NoError>//登录的action
init(_ signal1 : Signal<String?, NoError> , _ signal2 : Signal<String?, NoError>) {
//获取用户名输入框和密码输入框文字变化的信号
userNameSignal = signal1
passWordSignal = signal2
//合并信号
validSignal = Signal.combineLatest(userNameSignal, passWordSignal).map{ $0!.characters.count >= 5 && $1!.characters.count >= 6}
//根据合并的信号,创建控制登录按钮enable的属性
let loginEnable = Property(initial: false, then: validSignal)
//通过.map对输入框变化的信号进行映射
let colorSignal : Signal<UIColor, NoError> = signal1.map { text in
return (text?.characters.count)! > 3 ? .white : .red
}
//根据信号创建textfield的颜色属性
tfColor = Property(initial: .white, then: colorSignal)
loginAction = Action(enabledIf: loginEnable) {
username, password in
return SignalProducer<Bool, NoError> { observer, disposable in
let success = (username == "admin") && (password == "password")
//网络请求
observer.send(value: success)
observer.sendCompleted()
}
}
}
}
Signal
//获取用户名输入框和密码输入框文字变化的信号
userNameSignal = signal1
passWordSignal = signal2
这里的signal其实是userNameTF.reactive.continuousTextValues传过来的,下文会说到。这里的信号是String类型,大家也可以通过.map方法将text的值映射成其他类型,例如:
let colorSignal : Signal<UIColor, NoError> = userNameSignal.map {
text in
return (text?.characters.count)! > 6 ? .white : .red
}
合并信号
validSignal = Signal.combineLatest(userNameSignal, passWordSignal).map{
$0!.characters.count >= 5 && $1!.characters.count >= 6
}
上面的代码中我们用Signal.combineLatest方法将usernameSignal和passwordSignal两个信号结合在一起,再将它们映射成一个Bool信号来表明username text field和password text field是否同时合法。然后可以绑定给button,控制button的isEnable属性,后面会说到。
Property
let loginEnable = Property(initial: false, then: validSignal)
Property首先接收一个初始的值,我们设置成false,之后这个property会随着signUpActiveSignal里面的值变化而变化。
Action
loginAction = Action(enabledIf: loginEnable) {
username, password in
return SignalProducer<Bool, NoError> { observer, disposable in
let success = (username == "admin") && (password == "password")
//网络请求
observer.send(value: success)
observer.sendCompleted()
}
}
Action是一个泛型为Action<Input, Output, SwiftError>,Action就是动作的意思,比如当用户点击了signInButton后应该发生的动作。Action可以有输入和输出,也可以没有。
上面的代码中,我们的输入来自username text field和password text field,输出为Bool(登录是否成功)。
/// Initializes an action that will be conditionally enabled, and creates a
/// `SignalProducer` for each input.
///
/// - parameters:
/// - enabledIf: Boolean property that shows whether the action is
/// enabled.
/// - execute: A closure that returns the signal producer returned by
/// calling `apply(Input)` on the action.
public convenience init<P: PropertyProtocol>(enabledIf property: P, _ execute: @escaping (Input) -> SignalProducer<Output, Error>) where P.Value == Bool {
self.init(state: property, enabledIf: { $0 }) { _, input in
execute(input)
}
}
enabledIf这个参数是一个bool的property,这个参数用来控制这个action的启用/禁用。后面一个参数是一个closure,它的原型为(Input) -> SignalProducer<Output, Error>),这个closure的Input来自于我们的username text field和password text field的值。
我们还需要为它返回一个SignalProducer<Output, Error>以便于添加副作用,例如网络请求是否成功等。
VC
先上代码
class ViewController: UIViewController {
@IBOutlet weak var userNameTF: UITextField!
@IBOutlet weak var passWordTF: UITextField!
@IBOutlet weak var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
bindViewModel()
}
func bindViewModel() -> () {
//初始化vm
let viewModel = LoginViewModel.init(userNameTF.reactive.continuousTextValues, passWordTF.reactive.continuousTextValues)
//把信号绑定给登录button
loginButton.reactive.isEnabled <~ viewModel.validSignal
//把颜色属性绑定给Textfield
userNameTF.reactive.backgroundColor <~ viewModel.tfColor
//通过CocoaAction实现button的点击
loginButton.reactive.pressed = CocoaAction<UIButton>(viewModel.loginAction){
_ in
return (self.userNameTF.text!, self.passWordTF.text!)
}
//观察登录是否成功
viewModel.loginAction.values.observeValues({ success in
if success {
print("login : \(success)" )
//VC跳转
}
})
}
}
属性绑定
loginButton.reactive.isEnabled <~ viewModel.validSignal
userNameTF.reactive.backgroundColor <~ viewModel.tfColor
关于<~,官方文档是这样写的:
<~运算符是提供了几种不同的绑定属性的方式。注意这里绑定的属性必须是 MutablePropertyType类型的。
property <~ signal将一个属性和信号绑定在一起,属性的值会根据信号送过来的值刷新。
property <~ producer 会启动这个producer,并且属性的值也会随着这个产生的信号送过来的值刷新。
property <~ otherProperty将一个属性和另一个属性绑定在一起,这样这个属性的值会随着源属性的值变化而变化。
留意XXX.reactive.xxx 如果这个属性基本上是BindingTarget类型,可以作为绑定目标类型,写在<~的左边
CocoaAction
loginButton.reactive.pressed = CocoaAction<UIButton>(viewModel.loginAction){
_ in
return (self.userNameTF.text!, self.passWordTF.text!)
}
viewModel.loginAction.values.observeValues({ success in
if success {
print("login : \(success)" )
//VC跳转
}
})
loginButton.reactive.pressed是一个CocoaAction,当用户点击了signInButton就会触发这个CocoaAction,CocoaAction同时会帮你控制loginButton的启用/禁用状态。
最后通过action.values.observeValues我们可以观察到loginAction事件,也就是我们登录成功与否的值。
最后
MVVM中VM和C就大概这样,一个基本的登录逻辑就完成,代码非常少。ReactiveSwift和ReactiveCocoa是好东西,就是作者写的文档太少和太混乱了。。。
上面有什么错漏,欢迎指正哈!