Password Validation Screen in SwiftUI (2/3)

Bartłomiej Lańczyk
5 min readApr 5, 2023

--

In the previous article, we created a view that allows the user to enter and confirm their password. In this article, we will add a model to it for validating the entered data using the Combine library.

Let’s get straight to the point then.

First, we create an ObservableObject final class and move all fields related to user input into it:

  • two for textfields
  • five for checkboxes
  • and one additional field indicating that all fields are valid
final class AuthenticationViewModel: ObservableObject {
// MARK: - Password input
@Published var password = ""
@Published var confirmPassword = ""

// MARK: - Password requirements
@Published var hasEightChar = false
@Published var hasSpacialChar = false
@Published var hasOneDigit = false
@Published var hasOneUpperCaseChar = false
@Published var confirmationMatch = false
@Published var areAllFieldsValid = false
}

Next, let’s remove the state values from our view and initialize our class as @StateObject, which is perfectly fine in this simple example.

Our SignUpPasswordScreen should looks like below:

struct SignUpPasswordScreen: View {
@StateObject private var authVM = AuthenticationViewModel()

var body: some View {
VStack(spacing: 0) {
VStack {
VStack(alignment: .leading, spacing: 10) {
Text("Password")
.font(.title)
.bold()
Text("Password must have more than 8 characters, contain some special character, one digit, one uppercase letter")
.font(.caption)
}
Group {
UserFormTextField(text: $authVM.password, type: .password)
VStack(alignment: .leading) {
RequirementsPickerView(type: .eightChar, toggleState: $authVM.hasEightChar)
RequirementsPickerView(type: .spacialChar, toggleState: $authVM.hasSpacialChar)
RequirementsPickerView(type: .oneDigit, toggleState: $authVM.hasOneDigit)
RequirementsPickerView(type: .upperCaseChar, toggleState: $authVM.hasOneUpperCaseChar)
}
UserFormTextField(text: $authVM.confirmPassword, type: .repeatPassword)
RequirementsPickerView(type: .confirmation, toggleState: $authVM.confirmationMatch)
// ...

Validations

Now we move on to the heart of the example, which is validating the fields. Each requirement represented by a blue checkbox on the view will cooperate with a single subscription. Then we will use the helpful CombineLatest functionality to combine the stream associated with the password and password confirmation to validate the latest value.

But let’s start step by step:

Minimum 8 characters

We simply check if the number of characters is greater than or equal to 8 using count.

$password
.map { password in
password.count >= 8
}
.assign(to: &$hasEightChar)

Minimum 1 special character

We create CharacterSet consisting of the special characters that are commonly used in passwords. Then rangeOfCharacter(from:) method of the String class to check if any of these special characters are present in the password. If at least one special character is found, the method returns a non-nil value, indicating that the condition is met.

$password
.map { password in
password.rangeOfCharacter(from: CharacterSet(charactersIn: "!@#$%^&*()_+-=[]{}|:\"';<>,.?/~`")) != nil
}
.assign(to: &$hasSpacialChar)

Minimum 1 digit

Here we are using a value provided by the standard library which checks whether a character represents a numeric value.

$password
.map { password in
password.contains { $0.isNumber }
}
.assign(to: &$hasOneDigit)

Minimum 1 uppercase letter

Similar to the above but for uppercase characters.

$password
.map { password in
password.contains { $0.isUppercase }
}
.assign(to: &$hasOneUpperCaseChar)

Confirmation match password

In the penultimate step, we will use CombineLatest to combine two data streams coming from the password and password confirmation fields and compare whether their values are equal, returning true if they match, or false if they do not.

Publishers.CombineLatest($password, $confirmPassword)
.map { [weak self] _, _ in
guard let self else { return false}
return self.password == self.confirmPassword
}
.assign(to: &$confirmationMatch)

All fields match

In the final step, we take a brief look at everything we have done so far 😉. In reality, we combine two streams as in the step above, and assign a value of true to the areAllFieldsValid state if all other states return true, or false if at least one is not set to true.

Publishers.CombineLatest($password, $confirmPassword)
.map { [weak self] _, _ in
guard let self else { return false}
return self.hasEightChar && self.hasSpacialChar && self.hasOneDigit && self.hasOneUpperCaseChar && self.confirmationMatch
}
.assign(to: &$areAllFieldsValid)

Then we wrap our validators in a private function and call it in the class initializer.

Our AuthenticationViewModel class should looks like this:

import Combine
import Foundation

final class AuthenticationViewModel: ObservableObject {
// MARK: - Password input
@Published var password = ""
@Published var confirmPassword = ""

// MARK: - Password requirements
@Published var hasEightChar = false
@Published var hasSpacialChar = false
@Published var hasOneDigit = false
@Published var hasOneUpperCaseChar = false
@Published var confirmationMatch = false
@Published var areAllFieldsValid = false

init() {
validateSignUpFields()
}

private func validateSignUpFields() {
/// Check password has minimum 8 characters
$password
.map { password in
password.count >= 8
}
.assign(to: &$hasEightChar)
/// Check password has minimum 1 special character
$password
.map { password in
password.rangeOfCharacter(from: CharacterSet(charactersIn: "!@#$%^&*()_+-=[]{}|:\"';<>,.?/~`")) != nil
}
.assign(to: &$hasSpacialChar)
/// Check password has minimum 1 digit
$password
.map { password in
password.contains { $0.isNumber }
}
.assign(to: &$hasOneDigit)
/// Check password has minimum 1 uppercase letter
$password
.map { password in
password.contains { $0.isUppercase }
}
.assign(to: &$hasOneUpperCaseChar)
/// Check confirmation match password
Publishers.CombineLatest($password, $confirmPassword)
.map { [weak self] _, _ in
guard let self else { return false}
return self.password == self.confirmPassword
}
.assign(to: &$confirmationMatch)
/// Check all fields match
Publishers.CombineLatest($password, $confirmPassword)
.map { [weak self] _, _ in
guard let self else { return false}
return self.hasEightChar && self.hasSpacialChar && self.hasOneDigit && self.hasOneUpperCaseChar && self.confirmationMatch
}
.assign(to: &$areAllFieldsValid)
}
}

And here is our final screen in action:

Conclusion

We have reached the end. This prepared screen looks good and informs the user about the stage of entering the password, which many websites and applications do in an annoying way. Thanks to the use of the Combine library, we were able to divide the individual parts of our validators, which makes the model very easily extensible, for example with email validation or other fields. Thank you for your attention.

Full code is available on Github: https://github.com/miltenkot/EmailAuthenticateScreen

Part 1:

https://medium.com/@miltenkot/how-to-create-a-password-validation-screen-in-swiftui-part-1-9ee1765f90a7

If you enjoyed this article and would like to see more, please leave a reaction.

--

--