Demo布景
表单验证也是一个频次很高的交互场景,一般套路是监测用户输入的邮箱格局是否正确,暗码是否有值,如果邮箱格局正确而且有输入暗码的情况下,登录按钮的状况才变成可交互状况,不然就一直是不可点击的状况。一般会用Notification监测文本框的输入,每次有值改动,就去调用相对应的验证办法,一切的办法都走一遍后输出一个按钮是否可点击的bool值,这么做能够,但总觉得不是很优雅,于是尝试用Combine来完成一版,详细的交互如下:
Combine
Combine是iOS13上出来的一个sdk,江湖上号称是呼应式函数编程在iOS中的一种完成。有两个重要的人物:publisher和subscriber。publisher用来expose值,官方文档说是 for processing values over time。应该是说能够在整个时刻维度上随时抛出有变化的值,是一个流(stream)的概念。subscriber被用来接纳publisher抛出的值。
详细步骤
- UI层面简单用storyboard布局,email、password两个文本框,一个登录的button和一个默认躲藏用来显示登录状况的label
- 创建一个LoginViewModel,一致处理要监听的特点和Combine中一些api的调用以及一些逻辑代码(比方邮箱格局是否正确,登录按钮是否可用等)
- viewController中去observer,接纳特点值的变化,实时update UI
LoginViewModel
import Foundation
import Combine
class LoginViewModel: ObservableObject{
// sec 1
enum ViewState {
case loading
case success
case failed
case none
}
// sec 2
@Published var email = ""
@Published var password = ""
@Published var state: ViewState = .none
// sec 3
var isValidUsernamePublisher: AnyPublisher<Bool, Never>{
$email.map{ $0.isValidEmail}.eraseToAnyPublisher()
}
var isValidPasswordPublisher: AnyPublisher<Bool, Never>{
$password.map{!$0.isEmpty}.eraseToAnyPublisher()
}
// sec 4
var isSubmitEnabled: AnyPublisher<Bool, Never>{
Publishers.CombineLatest(isValidUsernamePublisher, isValidPasswordPublisher).map{ $0 && $1 }.eraseToAnyPublisher()
}
// sec 5
func submitLogin(){
state = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)){[weak self] in
guard let self = self else {return}
if self.isCorrectLogin(){
self.state = .success
}else{
self.state = .failed
}
}
}
// sec 6
func isCorrectLogin() -> Bool{
return email == "pgyjyl@163.com" && password == "123456"
}
}
// sec 7
extension String{
var isValidEmail: Bool{
return NSPredicate(
format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
).evaluate(with: self)
}
}
- sec1 定义ViewState,区别view的不同状况
- sec2 LoginViewModel先要继承ObservableObject,才干有才能抛出值的改动,要监听改动的特点需要用特点包装器 @Published 润饰。这些被Published的特点只需产生变动就能实时驱动view状况的改动,达到updateUI的作用。
- sec3 isValidUsernamePublisher和isValidPasswordPublisher便是对详细业务逻辑的处理了,详细逻辑在map回调里。
- eraseToAnyPublisher:map的block里处理完业务逻辑,终究应该回来一个bool变量出来,eraseToAnyPublisher办法会包装真实的回来类型为一个通用的AnyPublisher<Bool, Never>类型,更好的被一切observe实时监听
- sec4 CombineLatest:Combine sdk的信合api了,终究按钮是否可用的输出口是调用CombineLatest办法,传入若干个publisher,根据入参的不同回来不同的tuple,demo中会回来一个(string,string)的tuple,然后用map办法,把tuple转换成一个bool的表示
- sec5 登录按钮的点击回调,更新state状况
- sec6 匹配到写死的email和password 才算登录成功
- sec7 String的extension办法,正则验证一个字符串是否符合email的规矩
ViewController的更新
class ViewController: UIViewController {
var viewModel = LoginViewModel()
var cancellables = Set<AnyCancellable>()
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var submitButton: UIButton!
@IBOutlet weak var errorLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupPublishers()
}
func setupPublishers(){
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: emailTextField)
.map{($0.object as! UITextField).text ?? ""}
.assign(to: \.email, on: viewModel)
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordTextField)
.map{($0.object as! UITextField).text ?? ""}
.assign(to: \.password, on: viewModel)
.store(in: &cancellables)
viewModel.isSubmitEnabled
.assign(to: \.isEnabled, on: submitButton)
.store(in: &cancellables)
viewModel.$state.sink{[weak self] state in
switch state{
case .loading:
self?.submitButton.isEnabled = false
self?.submitButton.setTitle("Loading..", for: .normal)
self?.hideError(true)
case .success:
self?.showResultScreen()
self?.resetButton()
self?.hideError(true)
case .failed:
self?.resetButton()
self?.hideError(false)
case .none:
break
}
}.store(in: &cancellables)
}
@IBAction func onSubmit(_ sender: Any) {
viewModel.submitLogin()
}
func resetButton(){
submitButton.setTitle("Login", for: .normal)
submitButton.isEnabled = true
}
func showResultScreen(){
let resultVc = ResultViewController()
navigationController?.pushViewController(resultVc, animated: true)
}
func hideError(_ isHidden: Bool){
errorLabel.alpha = isHidden ? 0 : 1
}
}
- 首先声明一个LoginViewModel的特点 viewModel
- AnyCancellable的set,让subscriber能够随时release一个publisher
- setupPublishers:
- 使用notification这个publisher监听email和password两个文本框的值改动,在assign中,文本框的值被更新到viewModel中要监听的特点上(@Published润饰的特点),而且observe被store在cancellables的set中,这是publisher的一个链式调用,本质上做了observe和publisher的绑定
- isSubmitEnabled:将按钮是否可用的publisher assgin on到详细的button上
- 其实内部是keypath assing subscription。是我们创建接纳publisher并设置状况到详细的UIKit上的subscription的一个办法,回来值是An
AnyCancellable
instance
- 其实内部是keypath assing subscription。是我们创建接纳publisher并设置状况到详细的UIKit上的subscription的一个办法,回来值是An
- sink:viewModel.$state.sink 回来值也是AnyCancellable,首要用来输出值的一个subscription,根据不同的输出值的状况更新view
引证
- Apple’sCombinedocumentation
- Form Validation in UIKit Made Easy With Combine
- Getting Started With Combine