承接上一章的内容,这一章,我们实现一下Combine
异步编程框架和MVVM
开发模式。
我们来看下登录页面有哪些元素:用户名、密码、再次输入密码。
接下来,每一个元素的校验规则我们定一下:
用户名:至少需要2个字符;
密码:至少需要6位数,而且需要有一位是大写;
再次输入密码:需要和密码相同;
数据模型创建
我们创建一个新的swift
文件,命名为ModelView.swift
,用来作为ModelView
文件。
class ViewModel: ObservableObject { // 输入 @Published var username = "" @Published var password = "" @Published var passwordConfirm = "" // 输出 @Published var isUsernameLengthValid = false @Published var isPasswordLengthValid = false @Published var isPasswordCapitalLetter = false @Published var isPasswordConfirmValid = false }
我们创建了一个ModelView
类,它符合ObservableObject
协议,然后使用了@Published
注释username
用户名、password
密码和passwordConfirm
二次密码,当我们的值发生变化的时候,系统会通知订阅者执行相应的校验。
校验规则-订阅
好了,数据模型建立好了,我们继续完成数据校验规则的部分。
首先,我们试试完成用户名的校验,当username
用户名发生改变的时候,我们将结果告诉isUsernameLengthValid
。
然后,在这里我们使用到的就是Combine
异步编程框架,首先需要引入import Combine
,然后在init()
方法中完成代码。
Combine
框架提供了两个内置订户:接收和分配。接收器创建一个通用订阅者来接收值;分配器创建特定属性,用来更新数据对象。例如,它将验证结果(true/false
)直接赋值给isUsernameLengthValid
。
init() { //用户名校验 $username .receive(on: RunLoop.main) .map { username in return username.count >= 2 } .assign(to: \.isUsernameLengthValid, on: self) }
在上面的代码中,$username
是我们需要操作的监听的数据源,我们调用receive(on:xxxx)
函数来确保订阅者在主线程RunLoop
上接收到它的值。
map
函数是Combine
中的操作符,它接受输入、处理输入并将输入转换为订阅者所期望的内容,也就是判断username
用户名至少2
个字符。
最后,我们将验证结果作为布尔值(true/false
)返回给订阅者。
同理,我们完成密码、密码二次确认的代码。
init() { //用户名校验 $username .receive(on: RunLoop.main) .map { username in return username.count >= 2 } .assign(to: \.isUsernameLengthValid, on: self) //密码校验 $password .receive(on: RunLoop.main) .map { password in return password.count >= 6 } .assign(to: \.isPasswordLengthValid, on: self) //密码大写校验 $password .receive(on: RunLoop.main) .map { password in let pattern = "[A-Z]" if let _ = password.range(of: pattern, options: .regularExpression) { return true } else { return false } } .assign(to: \.isPasswordCapitalLetter, on: self) }
第一个订阅者订阅密码长度的验证结果,我们分配给isPasswordLengthValid
属性。
第二个用于处理大写字母的验证,我们使用range
方法来测试密码是否至少包含一个大写字母,然后分配给isPasswordCapitalLetter
属性。
对于密码和密码二次确认,由于password
和passwordConfirm
都是发布者,我们需要验证两个发布者是否具有相同的值,我们使用Publisher.combingLatest
来接收和组合来自发布者的最新值,然后验证这两个值是否相同。
//两次密码是否相同 Publishers.CombineLatest($password, $passwordConfirm).receive(on: RunLoop.main) .map { password, passwordConfirm in !passwordConfirm.isEmpty && (passwordConfirm == password) } .assign(to: \.isPasswordConfirmValid, on: self)
校验规则-取消
完成了基于Combine
异步编程框架订阅后,我们还需要完成取消订阅的操作,以便于我们在ModelView
类初始化的时候更新UI。
我们需要定义一个取消订阅的数组,把可以被取消的引用全部包裹在里面。
private var cancellableSet: Set<AnyCancellable> = []
然后在每一个校验代码后面都加上.store
修饰。
.store(in: &cancellableSet)
store
函数允许我们将可取消引用保存到一个集合中,以便以后进行清理。如果不存储引用,可能会出现内存泄漏问题。
校验规则-引用
接下来,我们可以在ContentView
主视图中引用校验规则。
由于我们在ModelView
中定义好了我们需要的属性,username
用户名、password
密码和passwordConfirm
二次密码。那么我们就可以直接引用ModelView
,然后删掉之前用@State
定义的参数。
@ObservedObject private var viewModel = ViewModel()
然后校验规则的绑定上,我们将原有的$
绑定关系,修订为$viewModel.XXXX
绑定关系。
以及我们可以根据订阅者接收返回的值,示例isUsernameLengthValid
,判读是否显示错误提醒。
//用户名 VStack { RegistrationView(isTextField: true, fieldName: "用户名", fieldValue: $viewModel.username) if viewModel.isUsernameLengthValid { InputErrorView(iconName: "exclamationmark.circle.fill", text:"用户不存在") } }
恭喜你,完成了本章的所有练习~
章节中可能有存在校验规则的一些小错误,这里也懒得改了,就当作留个小作业给到童鞋们吧!
完整代码
//ViewModel.swift import Combine import Foundation class ViewModel: ObservableObject { // 输入 @Published var username = "" @Published var password = "" @Published var passwordConfirm = “" // 输出 @Published var isUsernameLengthValid = false @Published var isPasswordLengthValid = false @Published var isPasswordCapitalLetter = false @Published var isPasswordConfirmValid = false //取消订阅 private var cancellableSet: Set<AnyCancellable> = [] init() { //用户名校验 $username .receive(on: RunLoop.main) .map { username in username.count >= 2 } .assign(to: \.isUsernameLengthValid, on: self) .store(in: &cancellableSet) //密码校验 $password .receive(on: RunLoop.main) .map { password in password.count >= 6 } .assign(to: \.isPasswordLengthValid, on: self) .store(in: &cancellableSet) //密码大写校验 $password .receive(on: RunLoop.main) .map { password in let pattern = "[A-Z]" if let _ = password.range(of: pattern, options: .regularExpression) { return true } else { return false } } .assign(to: \.isPasswordCapitalLetter, on: self) .store(in: &cancellableSet) //两次密码是否相同 Publishers.CombineLatest($password, $passwordConfirm).receive(on: RunLoop.main) .map { password, passwordConfirm in !passwordConfirm.isEmpty && (passwordConfirm == password) } .assign(to: \.isPasswordConfirmValid, on: self) .store(in: &cancellableSet) } }
//ContentView.swift import SwiftUI struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { VStack (alignment: .leading, spacing: 40) { //用户名 VStack { RegistrationView(isTextField: true, fieldName: "用户名", fieldValue: $viewModel.username) if !viewModel.isUsernameLengthValid { InputErrorView(iconName: "exclamationmark.circle.fill", text:"用户不存在") } } //密码 VStack{ RegistrationView(isTextField: false, fieldName: "密码", fieldValue: $viewModel.password) if !viewModel.isPasswordLengthValid && !viewModel.isPasswordCapitalLetter { InputErrorView(iconName: "exclamationmark.circle.fill", text: viewModel.isPasswordCapitalLetter ? "密码不正确" : "密码需要有一位大写") } } //再次输入密码 VStack { RegistrationView(isTextField: false, fieldName: "再次输入密码", fieldValue: $viewModel.passwordConfirm) if !viewModel.isPasswordConfirmValid { InputErrorView(iconName: "exclamationmark.circle.fill", text: "两次密码需要相同") } } //注册按钮 Button(action: { }) { Text("注册") .font(.system(.body, design: .rounded)) .foregroundColor(.white) .bold() .padding() .frame(minWidth: 0, maxWidth: .infinity) .background(Color(red: 51 / 255, green: 51 / 255, blue: 51 / 255)) .cornerRadius(10) .padding(.horizontal) } }.padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } //注册视图 struct RegistrationView:View { var isTextField = false var fieldName = "" @Binding var fieldValue: String var body: some View { VStack { //判断是不是输入框 if isTextField { //输入框 TextField(fieldName, text: $fieldValue) .font(.system(size: 20, weight: .semibold)) .padding(.horizontal) } else { //密码输入框 SecureField(fieldName, text: $fieldValue) .font(.system(size: 20, weight: .semibold)) .padding(.horizontal) } //分割线 Divider() .frame(height: 1) .background(Color(red: 240/255, green: 240/255, blue: 240/255)) .padding(.horizontal) } } } //错误判断 struct InputErrorView:View { var iconName = "" var text = "" var body: some View { HStack { Image(systemName: iconName) .foregroundColor(Color(red: 251/255, green: 128/255, blue: 128/255)) Text(text) .font(.system(.body, design: .rounded)) .foregroundColor(Color(red: 251/255, green: 128/255, blue: 128/255)) Spacer() }.padding(.leading,10) } }
快来动手试试吧!