承接上一章的内容,这一章,我们实现一下​​Combine​​​异步编程框架和​​MVVM​​开发模式。

我们来看下登录页面有哪些元素:用户名、密码、再次输入密码。

SwiftUI极简教程31: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

SwiftUI极简教程31:Combine异步编程框架和MVVM开发模式的使用(下)_用户名_02

我们创建了一个​​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​​函数允许我们将可取消引用保存到一个集合中,以便以后进行清理。如果不存储引用,可能会出现内存泄漏问题。

SwiftUI极简教程31:Combine异步编程框架和MVVM开发模式的使用(下)_异步编程_03

校验规则-引用

接下来,我们可以在​​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:"用户不存在")
}
}

SwiftUI极简教程31:Combine异步编程框架和MVVM开发模式的使用(下)_用户名_04

恭喜你,完成了本章的所有练习~

章节中可能有存在校验规则的一些小错误,这里也懒得改了,就当作留个小作业给到童鞋们吧!

完整代码

//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)
}
}

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~