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

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

image.png

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

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

image.png

接下来,每一个元素的校验规则我们定一下:


用户名:至少需要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
}


image.png


我们创建了一个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属性。

对于密码和密码二次确认,由于passwordpasswordConfirm都是发布者,我们需要验证两个发布者是否具有相同的值,我们使用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函数允许我们将可取消引用保存到一个集合中,以便以后进行清理。如果不存储引用,可能会出现内存泄漏问题。

image.png

校验规则-引用


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


image.png

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

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

完整代码


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

快来动手试试吧!


相关文章
|
3月前
|
Dart 数据安全/隐私保护
Flutter:一种简化表单验证高级技巧
Flutter:一种简化表单验证高级技巧
44 0
|
6月前
|
开发工具 Swift iOS开发
利用SwiftUI构建动态用户界面:iOS开发新范式
【4月更文挑战第3天】 随着苹果不断推进其软件开发工具的边界,SwiftUI作为一种新兴的编程框架,已经逐渐成为iOS开发者的新宠。不同于传统的UIKit,SwiftUI通过声明式语法和强大的功能组合,为创建动态且响应式的用户界面提供了一种更加简洁高效的方式。本文将深入探讨如何利用SwiftUI技术构建具有高度自定义能力和响应性的用户界面,并展示其在现代iOS应用开发中的优势和潜力。
|
Dart 前端开发 JavaScript
Flutter(二十二)——异步编程
Flutter(二十二)——异步编程
272 2
Flutter(二十二)——异步编程
|
缓存 前端开发 API
ReactiveCocoa 进阶,轻松搞定函数式编程框架
函数式编程已经变得越来越流行,而且也有很大的优势,作为iOS开发者,函数式编程框架**ReactiveCocoa**到底怎么使用呢, 接下来我们来深入介绍**ReactiveCocoa**及其在**MVVM**中的用法。
194 0
|
缓存 监控 Swift
函数式编程框架 ReactiveCocoa 基础入门
函数式编程框架 ReactiveCocoa 基础入门
140 0
|
前端开发 数据处理 API
SwiftUI极简教程30:Combine异步编程框架和MVVM开发模式的使用(中)
SwiftUI极简教程30:Combine异步编程框架和MVVM开发模式的使用(中)
936 0
SwiftUI极简教程30:Combine异步编程框架和MVVM开发模式的使用(中)
|
存储 前端开发 数据安全/隐私保护
SwiftUI极简教程29:Combine异步编程框架和MVVM开发模式的使用(上)
SwiftUI极简教程29:Combine异步编程框架和MVVM开发模式的使用(上)
764 0
SwiftUI极简教程29:Combine异步编程框架和MVVM开发模式的使用(上)
|
iOS开发
SwiftUI极简教程06:代码优雅复用
SwiftUI极简教程06:代码优雅复用
493 0
SwiftUI极简教程06:代码优雅复用
|
API Swift iOS开发
Swift5.0 - day13 - 响应式编程
Swift5.0 - day13 - 响应式编程
336 0
Swift5.0 - day13 - 响应式编程