这是我参与更文挑战的第27天,活动详情查看: 更文挑战
先看看UI效果
上图的Gif大概显示了这样一个流程:
1.在我的页面点击登录按钮,push到登录页面。
2.在登录页面点击还没有注册,push到注册页面,可以看集注册页面与登录页面非常的相似,只是多了一个请再次输入密码的textField。
3.回到登录页面,在手机号输入框,输入手机号码,这里只做了手机号的位数校验,那么第二行的密码输入框才会enable,在手机号位数正确,并且密码不为空的情况下,按钮才会enable。 注意录屏中对于密码输入看不见不是我造成的,而是系统录屏就是如此。
4.点击登录,进行登录的接口请求,请求成功后,pop到我的页面。
5.并在“登录成功”Toast后,接着请求我的积分接口,获取用的相关个人积分数据。
今天主要说登录与注册页面的编写以及登录页面的两个输入、一个按钮的绑定逻辑和请求。
编写登录与注册页面的基类页面
就上面分析的,我们可以看到其实登录与注册页面有一些相似之处,基于能少写代码就少写代码的原则,我们完全可以抽一个基类过来进行复用,里面包含2个输入框,1个按钮以及登录接口和注册接口。
import UIKit
import MBProgressHUD
class AccountBaseController: BaseViewController {
/// 懒加载用户名输入框 因为在子类中需要使用,所以没有使用private修饰
lazy var usernameFiled: UITextField = {
let textField = UITextField()
textField.layer.borderWidth = 0.5
textField.layer.borderColor = UIColor.gray.cgColor
textField.backgroundColor = .white
textField.keyboardType = .numberPad
textField.returnKeyType = .done
textField.font = UIFont.systemFont(ofSize: 15)
textField.placeholder = "请输入用户名"
let emptyView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1))
textField.leftView = emptyView
textField.rightView = emptyView
textField.leftViewMode = .always
textField.rightViewMode = .always
return textField
}()
/// 懒加载密码输入框
lazy var passwordField: UITextField = {
let textField = UITextField()
textField.layer.borderWidth = 0.5
textField.layer.borderColor = UIColor.gray.cgColor
textField.backgroundColor = .white
textField.returnKeyType = .done
textField.font = UIFont.systemFont(ofSize: 15)
textField.isSecureTextEntry = true
textField.placeholder = "请输入密码"
let emptyView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1))
textField.leftView = emptyView
textField.rightView = emptyView
textField.leftViewMode = .always
textField.rightViewMode = .always
return textField
}()
/// 懒加载行为按钮,登录页面为登录按钮, 注册页面为注册按钮
lazy var actionButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitleColor(.white, for: .normal)
button.isEnabled = false
button.layer.cornerRadius = 22
button.layer.masksToBounds = true
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
/// 布局,这里只布局了两个输入框,而行为按钮因为两个页面的不同,需要进行单独布局,就不在基类中进行了
private func setupUI() {
view.addSubview(usernameFiled)
usernameFiled.snp.makeConstraints { make in
make.top.equalTo(view).offset(kTopMargin + 16)
make.leading.equalTo(view).offset(16)
make.trailing.equalTo(view).offset(-16)
make.height.equalTo(44)
}
usernameFiled.layer.cornerRadius = 22
usernameFiled.layer.masksToBounds = true
view.addSubview(passwordField)
passwordField.snp.makeConstraints { make in
make.top.equalTo(usernameFiled.snp.bottom).offset(16)
make.leading.trailing.height.equalTo(usernameFiled)
}
passwordField.layer.cornerRadius = 22
passwordField.layer.masksToBounds = true
}
}
extension AccountBaseController {
/// 登录接口
func login(username: String, password: String) {
accountProvider.rx.request(AccountService.login(username, password))
.map(BaseModel<AccountInfo>.self)
.subscribe { baseModel in
if baseModel.errorCode == 0 {
/// 这里单例进行登录信息的保存会在后面的账户模块进行说明,这里先写出来,不影响流程
AccountManager.shared.saveLoginUsernameAndPassword(info: baseModel.data, username: username, password: password)
/// 通过GCD切到主线程用MB,这里我其实尝试在在主线程订阅并使用MB,但是还是有问题,没办法祭出了GCD,有经验的小伙伴望可以教教我。
DispatchQueue.main.async {
MBProgressHUD.showText("登录成功")
}
/// 回退页面
self.navigationController?.popToRootViewController(animated: true)
}
} onError: { _ in
}.disposed(by: rx.disposeBag)
}
/// 注册接口,在注册成功后立刻调用登录接口
func registerAndLogin(username: String, password: String, repassword: String) {
accountProvider.rx.request(AccountService.register(username, password, repassword))
.map(BaseModel<AccountInfo>.self)
.subscribe { baseModel in
if baseModel.errorCode == 0 {
DispatchQueue.main.async {
MBProgressHUD.showText("注册成功")
}
self.login(username: username, password: password)
}
} onError: { _ in
}.disposed(by: rx.disposeBag)
}
}
复制代码
在AccountBaseController中,并没有什么特别复杂的逻辑,就是登录接口的订阅中:
-
我提前将AccountManager的部分逻辑写出来了,但是不影响流程(账户管理模块)。
-
另外在订阅中通过GCD切换到主线程使用MB,我尝试在主线程订阅,不使用GCD弹MB,但是有问题,如果有知道的大佬还麻烦教教我。
这里我也特别想了解,如何在RxSwift中的订阅中正确的使用MBProgressHUD?
登录页面
有了上面这个AccountBaseController,我们写起LoginControll基本上更加关注的是两个输入框与登录按钮的使能关系逻辑上来。注意看注释喔。
import UIKit
import RxSwift
import RxCocoa
import MBProgressHUD
class LoginController: AccountBaseController {
/// 懒加载还没有注册按钮
private lazy var toRegisterButton: UIButton = {
let button = UIButton(type: .custom)
let attString = NSAttributedString(string: "还没有注册?", attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue, .foregroundColor: UIColor.systemBlue, .font: UIFont.systemFont(ofSize: 15)])
button.setAttributedTitle(attString, for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
/// 标题赋值
title = "登录"
/// 行为按钮赋值
actionButton.setTitle(title, for: .normal)
/// 还没有注册按钮的布局
view.addSubview(toRegisterButton)
toRegisterButton.snp.makeConstraints { make in
make.top.equalTo(passwordField.snp.bottom).offset(16)
make.trailing.equalTo(usernameFiled)
}
/// 登录按钮的布局
view.addSubview(actionButton)
actionButton.snp.makeConstraints { make in
make.top.equalTo(toRegisterButton.snp.bottom).offset(16)
make.leading.trailing.height.equalTo(usernameFiled)
}
/// 用户名的有效性序列,这里特别通过对位数多了校验,字符串必须大于等于11位,通过截取显示过多输入,并返回true,少于11位返回false
let usernameValid = usernameFiled.rx.text.orEmpty
.map { [weak self] text -> Bool in
if text.count >= 11 {
print("超出了,进行截取")
self?.usernameFiled.text = String(text.prefix(11))
return true
}else {
return false
}
}
.share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default
/// 密码的有效性序列,这里设置是密码大于1位即可,大家可以自己玩,搞个正则也是可以的
let passwordValid = passwordField.rx.text.orEmpty
.map { $0.count > 0 }
.share(replay: 1)
/// 使用序列的combineLatest联合上面两个序列的,用于表示按钮的有效性
let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.share(replay: 1)
/// 用户名的有效性与密码输入框的使能绑定
usernameValid
.bind(to: passwordField.rx.isEnabled)
.disposed(by: rx.disposeBag)
/// 用户名的有效性map成为颜色与密码输入框的背景色绑定
usernameValid.map { $0 ? UIColor.white : UIColor.gray.withAlphaComponent(0.5) }
.bind(to: passwordField.rx.backgroundColor)
.disposed(by: rx.disposeBag)
/// 两个输入框联合的有效性与按钮的使能绑定
everythingValid
.bind(to: actionButton.rx.isEnabled)
.disposed(by: rx.disposeBag)
/// 两个输入框联合的有效性map成为颜色与按钮的背景色绑定
everythingValid.map { $0 ? UIColor.systemBlue : UIColor.systemBlue.withAlphaComponent(0.5) }
.bind(to: actionButton.rx.backgroundColor)
.disposed(by: rx.disposeBag)
/// 还没有注册按钮的点击事件
toRegisterButton.rx.tap
.subscribe { [weak self] _ in
self?.navigationController?.pushViewController(RegisterController(), animated: true)
}.disposed(by: rx.disposeBag)
/// 登录页面的点击事件,调用基类中的登录接口
actionButton.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let username = self?.usernameFiled.text,
let password = self?.passwordField.text else {
return
}
self?.login(username: username, password: password)
})
.disposed(by: rx.disposeBag)
}
}
复制代码
到此,登录页面的布局与逻辑基本上就完成了,在这个页面中我们更多的是同valid来进行各种绑定关系来完成页面的功能,大家可以试想一下,不使用RxSwift,而是用UITextField的delegate来做,会写多少代码,反正逻辑上会比这个复杂,因为我自己以前也是用原生写这种逻辑关系的。
另外我想说的是,这个输入绑定的逻辑,我是很早偷师了RxSwift中文官网的例子,嘻嘻。
注册页面
既然登录页面已经完成,那么注册页面不过是比登录页面稍许多些逻辑,这里就直接放代码了:
import UIKit
import RxSwift
import RxCocoa
class RegisterController: AccountBaseController {
private lazy var repasswordField: UITextField = {
let textField = UITextField()
textField.layer.borderWidth = 0.5
textField.layer.borderColor = UIColor.gray.cgColor
textField.backgroundColor = .white
textField.returnKeyType = .done
textField.font = UIFont.systemFont(ofSize: 15)
textField.isSecureTextEntry = true
textField.placeholder = "请再次输入密码"
let emptyView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1))
textField.leftView = emptyView
textField.rightView = emptyView
textField.leftViewMode = .always
textField.rightViewMode = .always
return textField
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
title = "注册"
actionButton.setTitle(title, for: .normal)
view.addSubview(repasswordField)
repasswordField.snp.makeConstraints { make in
make.top.equalTo(passwordField.snp.bottom).offset(16)
make.leading.trailing.height.equalTo(usernameFiled)
}
repasswordField.layer.cornerRadius = 22
repasswordField.layer.masksToBounds = true
view.addSubview(actionButton)
actionButton.snp.makeConstraints { make in
make.top.equalTo(repasswordField.snp.bottom).offset(16)
make.leading.trailing.height.equalTo(usernameFiled)
}
let usernameValid = usernameFiled.rx.text.orEmpty
.map { [weak self] text -> Bool in
if text.count >= 11 {
print("超出了,进行截取")
self?.usernameFiled.text = String(text.prefix(11))
return true
}else {
return false
}
}
.share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default
let passwordValid = passwordField.rx.text.orEmpty
.map { $0.count > 0 }
.share(replay: 1)
let repasswordValid = repasswordField.rx.text.orEmpty
.map { $0.count > 0 }
.share(replay: 1)
let isSamePassword = Observable.combineLatest(passwordField.rx.text.orEmpty, repasswordField.rx.text.orEmpty) { $0 == $1 }.share(replay: 1)
let everythingValid = Observable.combineLatest(usernameValid, passwordValid, repasswordValid, isSamePassword) { $0 && $1 && $2 && $3 }
.share(replay: 1)
usernameValid
.bind(to: passwordField.rx.isEnabled)
.disposed(by: rx.disposeBag)
usernameValid.map { $0 ? UIColor.white : UIColor.gray.withAlphaComponent(0.5) }
.bind(to: passwordField.rx.backgroundColor)
.disposed(by: rx.disposeBag)
usernameValid.map { $0 ? UIColor.white : UIColor.gray.withAlphaComponent(0.5) }
.bind(to: repasswordField.rx.backgroundColor)
.disposed(by: rx.disposeBag)
everythingValid
.bind(to: actionButton.rx.isEnabled)
.disposed(by: rx.disposeBag)
everythingValid.map { $0 ? UIColor.systemBlue : UIColor.systemBlue.withAlphaComponent(0.5) }
.bind(to: actionButton.rx.backgroundColor)
.disposed(by: rx.disposeBag)
actionButton.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let username = self?.usernameFiled.text,
let password = self?.passwordField.text,
let repassword = self?.repasswordField.text else {
return
}
self?.registerAndLogin(username: username, password: password, repassword: repassword)
})
.disposed(by: rx.disposeBag)
}
}
复制代码
总结
我们今天进行了登录、注册页面的编写:
-
通过编写基类AccountBaseController,我们抽出两个页面特征相似的地方,并定义好接口。
-
登录页面,对登录页面独有的控件进行了布局,完成了各种绑定关系,已保证逻辑的正确性。
Observable.combineLatest
函数值得掌握其用法。 -
注册页面,页面内容与逻辑和登录页面非常相似,就没有多费笔墨。
明日继续
完成登录、注册页面后,其实我们需要对账户做一个跟踪管理,这个该如何实现是下一篇的内容。
大家加油!