承接上一章的内容,这一章,我们实现一下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
我们创建了一个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.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)
}
}
快来动手试试吧!
如果本专栏对你有帮助,不妨点赞、评论、关注~