我本来是打算学习 Combine 的,但因为网上的资料太少,所以决定先学习 RxSwift 作为过渡。但是一学便爱上啦,声明式编程真是太有爱啦~
这是 RxSwift 中文学习文档(我的例子是来源于这里) https://beeth0ven.github.io/RxSwift-Chinese-Documentation/
需求
这是一个模拟用户登录的程序。
- 当用户输入用户名时,如果用户名不足 5 个字就给出红色提示语,并且无法输入密码,当用户名符合要求时才可以输入密码。
- 同样的当用户输入的密码不到 5 个字时也给出红色提示语。
- 当用户名和密码有一个不符合要求时底部的绿色按钮不可点击,只有当用户名和密码同时有效时按钮才可点击。
- 当点击绿色按钮后弹出一个提示框,这个提示框只是用来做演示而已。
声明式编程实现
override func viewDidLoad() {
super.viewDidLoad()
usernameValidOutlet.text = "Username has to be at least \(minimalUsernameLength) characters"
passwordValidOutlet.text = "Password has to be at least \(minimalPasswordLength) characters"
/*
1. 输入用户名-》用户名是否有效-〉用户名提示是否隐藏/密码输入框是否可用
*/
let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.count >= minimalUsernameLength }
.share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default
usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)
usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)
/*
2. 输入密码-》密码是否有效-〉密码提示是否隐藏
*/
let passwordValid = passwordOutlet.rx.text.orEmpty
.map { $0.count >= minimalPasswordLength }
.share(replay: 1)
passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)
/*
3. 密码是否有效/用户名是否有效-》登录按钮是否可用
*/
let everythingValid = Observable
.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.share(replay: 1)
everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)
doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] _ in self?.showAlert() })
.disposed(by: disposeBag)
}
这分明就是把上面需求中的内容一条一条用 Swift 写了一遍而已呀!!!虽然例子简单,但是却让我体会到了领导的感觉。领导只需要说明要什么,不需要去费心思去考虑怎么做 😄
那再看看命令式编程怎么做
命令式编程实现
override func viewDidLoad() {
super.viewDidLoad()
// usernameOutlet.delegate = self
// passwordOutlet.delegate = self
usernameOutlet.addObserver(self, forKeyPath: "text", options: .new, context: nil)
passwordOutlet.addObserver(self, forKeyPath: "text", options: .new, context: nil)
doSomethingOutlet.addTarget(self, action: #selector(handleBtnAction(_:)), for: .touchUpInside)
}
@objc func handleBtnAction(_ btn: UIButton) {
showAlert()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let obj = object as? UITextField, let change = change?[.newKey] as? String else {
return
}
if obj == usernameOutlet {
nameChanged(change)
} else if obj == passwordOutlet {
psdChanged(change)
}
}
func nameChanged(_ name: String) {
if name.count < minimalUsernameLength {
usernameValidOutlet.isHidden = true
passwordValidOutlet.isEnabled = false
doSomethingOutlet.isEnabled = false
} else {
usernameValidOutlet.isHidden = false
passwordValidOutlet.isEnabled = true
// 第一种方法
// doSomethingOutlet.isEnabled = (passwordOutlet.text?.count ?? 0 > minimalPasswordLength)
// 第二种方法
doSomethingOutlet.isEnabled = passwordValidOutlet.isHidden
}
}
func psdChanged(_ psd: String) {
if psd.count < minimalPasswordLength {
passwordValidOutlet.isEnabled = false
doSomethingOutlet.isEnabled = false
} else {
passwordValidOutlet.isEnabled = true
doSomethingOutlet.isEnabled = true
}
}
// func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//
// }
- 这段代码可能是有问题的,甚至编译都编译不起来的,因为我没有 build,只是为了说明命令式编程怎么做而已
- 没有采用 UITextField 代理的方式是因为代理更麻烦,需要处理被注释掉的那个代理方法,而这个方法并不是直接把新的 text 值传进来的,需要我们自己根据输入框当前的值和被替换的信息自己得到,这很麻烦,所以我用了 KVO,无论行不行,意思到了就行
- 在用户名输入框内容符合要求之后,要更新doSomethingOutlet按钮的状态,此时按钮状态取决于密码输入框内容是否符合要求,获取密码输入框内容是否符合要求有两种办法:
- doSomethingOutlet.isEnabled = (passwordOutlet.text?.count ?? 0 > minimalPasswordLength)。这种方式不太好,需要把密码输入框是否符合要求的判断放在两个地方,而且这是个修改时容易忽略的地方
- doSomethingOutlet.isEnabled = passwordValidOutlet.isHidden。这种方式从逻辑上讲更不合适,因为按钮是否可用居然跟 passwordValidOutlet 是否隐藏有关,在不知道需求的情况下,这真的是非常奇怪的写法
声明式编程的好处我觉得不是在于语法上有多方便,不在于把视图创建和事件的处理代码放在一处很方便,而是逻辑上很简单清晰。
例子虽然简单,但是我在写命令式实现的时候,还是需要停下来想一想 isEnabled 是 true 还是 false 等等。而命令式编程已经让我看到产品经理一旦理好需求,似乎可以直接上手写代码啦 😄
机器人代替程序员写代码指日可待呀 😄