diff --git a/SwiftUIBasics/Views/Components/FormTextField.swift b/SwiftUIBasics/Views/Components/FormTextField.swift index cdd161c..35195e3 100644 --- a/SwiftUIBasics/Views/Components/FormTextField.swift +++ b/SwiftUIBasics/Views/Components/FormTextField.swift @@ -11,6 +11,7 @@ struct FormTextField: View { var name = "" @Binding var value: String var isSecure = false + var fieldType: UIKeyboardType = .default var body: some View { VStack { @@ -20,6 +21,7 @@ struct FormTextField: View { .padding(.horizontal) } else { TextField(name, text: $value) + .keyboardType(fieldType) .font(.system(size: 20, weight: .semibold, design: .rounded)) .padding(.horizontal) } diff --git a/SwiftUIBasics/Views/SignUpView.swift b/SwiftUIBasics/Views/SignUpView.swift index 6cef5d9..4b2cd3a 100644 --- a/SwiftUIBasics/Views/SignUpView.swift +++ b/SwiftUIBasics/Views/SignUpView.swift @@ -10,49 +10,89 @@ import Combine class SignUpViewModel: ObservableObject { // inputs - @Published var username: String = "" + @Published var email: String = "" @Published var password: String = "" @Published var passwordConfirm: String = "" // outputs - @Published var isValidUsernameLength: Bool = false + @Published var isValidEmail: Bool = false + @Published var isValidPasswordLength: Bool = false + @Published var isValidPasswordLowerCase: Bool = false @Published var isValidPasswordUpperCase: Bool = false + @Published var passwordHasSpecialSymbol: Bool = false + @Published var passwordHasNumber: Bool = false + @Published var isValidPassword: Bool = false + @Published var isValidPasswordMatch: Bool = false + @Published var isValid: Bool = false private var cancelableSet: Set = [] init() { - $username + $email .receive(on: RunLoop.main) - .map { username in - return username.count >= 4 + .map { email in + let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPredicate = NSPredicate(format:"SELF MATCHES %@", emailPattern) + return emailPredicate.evaluate(with: email) } - .assign(to: \.isValidUsernameLength, on: self) + .assign(to: \.isValidEmail, on: self) .store(in: &cancelableSet) $password .receive(on: RunLoop.main) .map { password in - return password.count >= 8 + let symbolRegex = Regex(/\W+/) + return password.contains(symbolRegex) } - .assign(to: \.isValidPasswordLength, on: self) + .assign(to: \.passwordHasSpecialSymbol, on: self) .store(in: &cancelableSet) $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 - } + let numberRegex = Regex(/[0-9]/) + return password.contains(numberRegex) + } + .assign(to: \.passwordHasNumber, on: self) + .store(in: &cancelableSet) + + $password + .receive(on: RunLoop.main) + .map { password in + let lowercaseRegex = Regex(/[a-z]/) + return password.contains(lowercaseRegex) + } + .assign(to: \.isValidPasswordLowerCase, on: self) + .store(in: &cancelableSet) + + $password + .receive(on: RunLoop.main) + .map { password in + let uppercaseRegex = Regex(/[A-Z]/) + return password.contains(uppercaseRegex) } .assign(to: \.isValidPasswordUpperCase, on: self) .store(in: &cancelableSet) + $password + .receive(on: RunLoop.main) + .map { password in + return password.count >= 8 + } + .assign(to: \.isValidPasswordLength, on: self) + .store(in: &cancelableSet) + + Publishers.CombineLatest4($isValidPasswordLength, $isValidPasswordUpperCase, $isValidPasswordLowerCase, $passwordHasSpecialSymbol, $passwordHasNumber) + .receive(on: RunLoop.main) + .map { (length, uppercase, lowercase, symbol, number) in + return length && uppercase && lowercase && symbol && number + } + .assign(to: \.isValidPassword, on: self) + .store(in: &cancelableSet) + Publishers.CombineLatest($password, $passwordConfirm) .receive(on: RunLoop.main) .map { (password, passwordConfirm) in @@ -61,9 +101,9 @@ class SignUpViewModel: ObservableObject { .assign(to: \.isValidPasswordMatch, on: self) .store(in: &cancelableSet) - Publishers.CombineLatest4($isValidUsernameLength, $isValidPasswordLength, $isValidPasswordUpperCase, $isValidPasswordMatch) - .map { (a, b, c, d) in - return a && b && c && d + Publishers.CombineLatest4($isValidEmail, $isValidPassword, $isValidPasswordMatch) + .map { (email, password, passwordMatch) in + return email && password && passwordMatch } .assign(to: \.isValid, on: self) .store(in: &cancelableSet) @@ -80,13 +120,16 @@ struct SignUpView: View { .bold() .foregroundStyle(.maryBlue) .padding(.bottom, 30) - FormTextField(name: "Username", value: $vm.username) - RequirementText(text: "A minimum of 4 characters", isValid: vm.isValidUsernameLength) + FormTextField(name: "Email", value: $vm.email, fieldType: .emailAddress) + RequirementText(text: "A valid email format", isValid: vm.isValidEmail) .padding() FormTextField(name: "Password", value: $vm.password, isSecure: true) VStack { RequirementText(text: "A minimum of 8 characters", isValid: vm.isValidPasswordLength) - RequirementText(text: "One uppercase letter", isValid: vm.isValidPasswordUpperCase) + RequirementText(text: "At least one uppercase letter", isValid: vm.isValidPasswordUpperCase) + RequirementText(text: "At lest one lowercase letter", isValid: vm.isValidPasswordLowerCase) + RequirementText(text: "At least one symbol", isValid: vm.passwordHasSpecialSymbol) + RequirementText(text: "At least one digit", isValid: vm.passwordHasNumber) } .padding() FormTextField(name: "Confirm Password", value: $vm.passwordConfirm, isSecure: true)