You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

解决方案

问题根源

  1. SecureInputView内的passwordsMatch用了@State,仅在初始化时赋值一次,外部值变化无法同步更新——@State是视图内部状态,静态传入的值无法触发视图刷新。
  2. 注册视图中手动调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 09:03:09