SwiftUI实现密码匹配时为文本框添加对勾标记
问题描述
我正在完善应用的认证模块,想要实现两次输入密码匹配时,在密码文本框显示对勾标记。
目前注册视图中调用了另一个文件的SecureInputView,checkPasswordsMatch函数能正确返回true/false,但SecureInputView不会跟着更新。我感觉已经接近实现了,可能是属性包装器用错了,但作为SwiftUI新手找不到问题,求帮忙。
1. 注册视图代码
import SwiftUI import SCLAlertView import Firebase import FirebaseAuth import FirebaseAnalytics import FirebaseAnalyticsSwift @MainActor final class SignUpEmailViewModel: ObservableObject { //INITIALIZE VARIABLES @Published var email: String = "" @Published var newPassword: String = "" @Published var newPasswordConfirm: String = "" @Published var passwordsMatch: Bool = false func signUp() async throws { //CONFIRM EMAIL FIELD IS NOT BLANK guard !email.isEmpty else { SCLAlertView().showError("Error", subTitle: "Please enter a valid email") return } //CONFIRM PASSWORD FIELDS ARE NOT BLANK guard !newPassword.isEmpty else { SCLAlertView().showError("Error", subTitle: "Please enter a valid password") // Error return } guard !newPasswordConfirm.isEmpty else { SCLAlertView().showError("Error", subTitle: "Please enter a valid password") // Error return } // RUN THE SIGN UP FUNCTION let returnedUserData = try await AuthenticationManager.shared.createUser(email: email, password: newPasswordConfirm) SCLAlertView().showSuccess("Success", subTitle: "Your account has been created. Welcome!") print(returnedUserData) } func checkPasswordsMatch(password1: String, password2: String) -> Bool { if password1 == password2 && password1 != "" && password2 != "" { print("Match") return true } else { print("NO MATCH") return false } } } struct SignUpEmailView: View { @ObservedObject private var viewModel = SignUpEmailViewModel() @Binding var showSignInView: Bool @State var agreeToTerms: Bool = false //MAIN BODY OF THE VIEW var body: some View { VStack(spacing: 8) { //APP LOGO Image(uiImage: UIImage(named: "logo_color_nobackground.png")!) .resizable() .aspectRatio(contentMode: .fit) .padding() //EMAIL FIELD TextField("Choose Email", text: $viewModel.email) .padding() .background(Color(.systemGray6)) .cornerRadius(10) .keyboardType(.emailAddress) .textContentType(.emailAddress) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) .frame(height: 55) .frame(maxWidth: .infinity) //PASSWORD FIELD 1 SecureInputView("Choose Password", text: $viewModel.newPassword, passwordsMatch: viewModel.checkPasswordsMatch(password1: viewModel.newPassword, password2: viewModel.newPasswordConfirm), isSingleField: false) .padding() .background(Color(.systemGray6)) .cornerRadius(10) .textContentType(.newPassword) .textInputAutocapitalization(.never) .disableAutocorrection(true) .frame(height: 55) .frame(maxWidth: .infinity) //PASSWORD FIELD 2 SecureInputView("Re-enter Password", text: $viewModel.newPasswordConfirm, passwordsMatch: viewModel.checkPasswordsMatch(password1: viewModel.newPassword, password2: viewModel.newPasswordConfirm), isSingleField: false) .padding() .background(Color(.systemGray6)) .cornerRadius(10) .textContentType(.newPassword) .textInputAutocapitalization(.never) .disableAutocorrection(true) .frame(height: 55) .frame(maxWidth: .infinity) //TOS AND PRIVACY POLICY BUTTON HStack { if agreeToTerms == true { Button { agreeToTerms = false } label: { Image(systemName: "checkmark.square.fill") .foregroundColor(.black) } } if agreeToTerms == false { Button { agreeToTerms = true } label: { Image(systemName: "square") .foregroundColor(.black) } } Text("I agree to the Terms of Service and Privacy Policy") .font(.caption) } .padding([.top]) //SIGN UP BUTTON Button { Task { do { if viewModel.newPassword == viewModel.newPasswordConfirm { try await viewModel.signUp() UserDefaults.standard.set(viewModel.email, forKey: "defaultEmail") showSignInView = false return } else { SCLAlertView().showError("Sign Up Error", subTitle: "Passwords must match. Please double check to ensure they do.") // Error } } catch { if let error = error as NSError? { firebaseErrorHandler(error: error) } } } } label: { Text("Sign Up") .font(.headline) .foregroundColor(.white) .frame(height: 55) .frame(maxWidth: .infinity) .background(agreeToTerms ? Color.blue : Color.gray) .cornerRadius(10) .padding() } .disabled(agreeToTerms == false) Spacer() } .padding() } } struct SignUpEmailView_Previews: PreviewProvider { static var previews: some View { SignUpEmailView(showSignInView: .constant(false)) } }
2. 包含SecureInputView的工具页代码
import Foundation import UIKit import SwiftUI import SCLAlertView import Firebase import FirebaseAuth import FirebaseAnalytics import FirebaseAnalyticsSwift final class Utilities { static let shared = Utilities() private init() {} @MainActor func topViewController(controller: UIViewController? = nil) -> UIViewController? { let controller = controller ?? UIApplication.shared.keyWindow?.rootViewController if let navigationController = controller as? UINavigationController { return topViewController(controller: navigationController.visibleViewController) } if let tabController = controller as? UITabBarController { if let selected = tabController.selectedViewController { return topViewController(controller: selected) } } if let presented = controller?.presentedViewController { return topViewController(controller: presented) } return controller } } //STRUCT TO BE ABLE TO VIEW THE SECUREFIELDS struct SecureInputView: View { @Binding private var text: String @State private var passwordsMatch: Bool = true @State private var isSecured: Bool = true private var isSingleField: Bool private var title: String init(_ title: String, text: Binding<String>, passwordsMatch: Bool, isSingleField: Bool) { self.title = title self._text = text self.passwordsMatch = passwordsMatch self.isSingleField = isSingleField } var body: some View { ZStack(alignment: .trailing) { Group { if isSecured { SecureField(title, text: $text) } else { TextField(title, text: $text) } } HStack { if isSingleField == false && passwordsMatch == true { Image(systemName: "checkmark") .foregroundColor(.green) } Button(action: { withAnimation (.easeIn(duration: 0.2)) { isSecured.toggle() } }) { Image(systemName: self.isSecured ? "eye.slash" : "eye") .accentColor(.gray) } } } } }
解决方案
问题根源
SecureInputView内的passwordsMatch用了@State,仅在初始化时赋值一次,外部值变化无法同步更新——@State是视图内部状态,静态传入的值无法触发视图刷新。- 注册视图中手动调用
checkPasswordsMatch属于静态计算,未与@Published属性绑定,无法驱动UI更新。
修复步骤
1. 优化ViewModel,自动维护密码匹配状态
删除单独的checkPasswordsMatch函数,用Combine监听密码字段变化,自动更新匹配状态:
@MainActor final class SignUpEmailViewModel: ObservableObject { @Published var email: String = "" @Published var newPassword: String = "" @Published var newPasswordConfirm: String = "" @Published var passwordsMatch: Bool = false init() { // 监听两个密码字段变化,自动更新匹配状态 $newPassword .combineLatest($newPasswordConfirm) .map { password1, password2 in !password1.isEmpty && !password2.isEmpty && password1 == password2 } .assign(to: &$passwordsMatch) } // 保留原有signUp函数 func signUp() async throws { // ... 原有逻辑不变 } }
2. 修改SecureInputView,接收绑定的匹配状态
将passwordsMatch改为@Binding,让视图能响应外部状态变化:
struct SecureInputView: View { @Binding private var text: String @Binding private var passwordsMatch: Bool // 改为@Binding @State private var isSecured: Bool = true private var isSingleField: Bool private var title: String // 初始化接收Binding类型的passwordsMatch init(_ title: String, text: Binding<String>, passwordsMatch: Binding<Bool>, isSingleField: Bool) { self.title = title self._text = text self._passwordsMatch = passwordsMatch self.isSingleField = isSingleField } // body部分保留不变 var body: some View { ZStack(alignment: .trailing) { Group { if isSecured { SecureField(title, text: $text) } else { TextField(title, text: $text) } } HStack { if !isSingleField && passwordsMatch { Image(systemName: "checkmark") .foregroundColor(.green) } Button(action: { withAnimation (.easeIn(duration: 0.2)) { isSecured.toggle() } }) { Image(systemName: isSecured ? "eye.slash" : "eye") .accentColor(.gray) } } } } }
3. 更新注册视图的SecureInputView调用
直接传入$viewModel.passwordsMatch,无需手动调用检查函数:
// 密码字段1 SecureInputView("Choose Password", text: $viewModel.newPassword, passwordsMatch: $viewModel.passwordsMatch, isSingleField: false) // ... 保留原有修饰符 // 密码字段2 SecureInputView("Re-enter Password", text: $viewModel.newPasswordConfirm, passwordsMatch: $viewModel.passwordsMatch, isSingleField: false) // ... 保留原有修饰符
修复逻辑说明
- ViewModel通过
combineLatest自动监听密码变化,维护匹配状态,避免手动调用检查函数,逻辑更简洁。 SecureInputView通过@Binding接收外部状态,状态变化时自动刷新UI,显示/隐藏对勾。- 整个流程为响应式:用户输入密码→ViewModel更新匹配状态→SecureInputView自动更新UI。
内容的提问来源于stack exchange,提问作者D Meyer
相关产品推荐
相关产品推荐

