@Ryanzi

View Original

[SwiftUI] Build a password setup view

Every application that requires you to create an account will have this simple page in its registration process, asking for the new password of your account. Depending on the security level of the application, the requirement for your password may be different (8, 9, or more characters long, “at least 1 uppercase/lowercase”, “at least 1 number”, “at least 1 symbol” and etc). I often feel mad after few attempts to type in a password but only found out they do not meet the requirement after I hit “submit” button. 

But I’ve also seen some password setup views that are very straightforward about the password rules and always has visual feedback as I’m typing to tell me whether my password is good or not. Some even indicate if the typed password is strong or weak. That’s a great UX practice to me so I’m trying to build one myself in my app, in SwiftUI.

The requirement for the view is simple: There will be two input fields, one is to type in the password, the other is to confirm the password in case the user had typos in it. The password should only be set if the content in both input fields matches. There should also be some visual indicators to show how the typed password meets the password rules. Preferably, one line of text for each rule, and a checkmark in front of it if that rule has been satisfied.


Let’s get started

I will skip the part that sets up a new SwiftUI project and adding the necessary assets and files, as there are tons of instructions for that already. Let’s say you have a SwiftUI file named PasswordSetupView in your project. Firstly we need to add two text input fields and a button into the view. Let’s use the normal TextField for now. Later I will change it to SecureField and have the “Show Password” button to switch them.

var body: some View {
    VStack(){
        TextField("Enter Password", text: $password)
            .textFieldStyle(PlainTextFieldStyle())
            .padding(5)
            .disableAutocorrection(true)
            .autocapitalization(.none)
        TextField("Re-enter Password", text: $confirmPassword)
            .textFieldStyle(PlainTextFieldStyle())
            .padding(5)
            .disableAutocorrection(true)
            .autocapitalization(.none)
        Button(action:{
            if password.count > 0 && password == confirmPassword {
                savePassword()
            }
        }) {
            HStack(){
                Text("Submit")
                    .font(.title2)
                    .fontWeight(.bold)
                    .foregroundColor(Color.blue)
            }
            .padding(10)
            .background(
                Capsule().strokeBorder(Color.blue, lineWidth: 2.0)
            )
            .accentColor(Color.white)
        }
    }
}

$password and $confirmPassword are two @State variables that binded with the two text input fields. We need to define them in the struct as well

struct PasswordSetupView: View {
    @State var password: String = ""
    @State var confirmPassword: String = ""

    var body: some View {
    ...
    }
}

The logic in the “Submit” button is simple: Save the password only if the password is not empty AND the password is equal to the confirmPassword.

func savePassword() -> Void {
    print("Saved password: \(password)")
    UserDefaults.standard.set(password, forKey: "KMASTER_PASSWORD")
}

Adding the policy-checking capability on the typing

A password policy can consist multiple rules, for example, it has to be “8 or more characters long” and “has at least 1 symbol” and “has at least 1 Uppercase”. We can define our PasswordPolicy as an OptionSet in Swift and each rule is an option in the set. Later we can combine any number of those options to form our password policy.

struct PasswordPolicy: OptionSet {
    let rawValue: Int8

    static let hasUppercase = PasswordPolicy(rawValue: 1<<0)
    static let hasLowercase = PasswordPolicy(rawValue: 1<<1)
    static let hasNumber    = PasswordPolicy(rawValue: 1<<2)
    static let hasSymbol    = PasswordPolicy(rawValue: 1<<3)
}

We define a policy variable to indicate what is the required policy. By default, we should have all of those options. The length of password is tracked separately, using minLength variable. Then we need a @State variable to track how the input password meets the specified policy.

var policy: PasswordPolicy = [.hasUppercase, .hasLowercase, .hasNumber, .hasSymbol]
@State var policyMeet: PasswordPolicy = []
var minLength: Int = 6

The policyMeet variable is used to control the visual feedback on the view as well. If the policy set contains a certain rule, say .hasUppercase, we show a line of text about that rule on the view. Then we keep updating the policyMeet set while user is typing the password. If the policyMeet contains that same rule we change the style of that Text to indicate the rule has been satisfied.

VStack(alignment: .leading, spacing: 2) {
            HStack() {
                Image(systemName: "checkmark.circle")
                    .font(.footnote)
                    .foregroundColor(.green)
                    .opacity(password.count >= minLength ? 1.0 : 0.0)
                Text("Must be \(minLength) characters or longer")
                    .font(.footnote)
                    .foregroundColor(password.count >= minLength ? .green : .red)
                    .opacity(1.0)
            }

            if policy.contains(.hasUppercase) {
                HStack() {
                    Image(systemName: "checkmark.circle")
                        .font(.footnote)
                        .foregroundColor(.green)
                        .opacity(policyMeet.contains(.hasUppercase) ? 1.0 : 0.0)
                    Text("Must contain at least 1 upper case")
                        .font(.footnote)
                        .foregroundColor(policyMeet.contains(.hasUppercase) ? .green : .red)
                }
            }
            ...Similar code for other rules...
}

To check if the typed password meets certain rules while the user is typing, we use the .onChange(of:) modifier of the TextField. The onCommit() action of the TextField is not ideal as it only trigged when you commit the change. Firstly we need to define the sets of characters for each rule. Ideally, we should use a Set but since the number of characters is small and limited I just used String for each character set. The checkPassword() is invoked in the .onChange(of:) action of the TextField so it is executed at each keystroke.

let lc = "abcdefghijklmnopqrstuvwxyz"  // for lowercase letters
let uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  // for uppercase letters
let nb = "0123456789"  // for numbers
let sb = "~`!@#$%^&*()-_=+{}[]:;<>,.?/"  // for symbols

func checkPassword() -> Void {
    policyMeet = []
    for l in password {
        if lc.contains(l) {policyMeet.insert(.hasLowercase)}
        if uc.contains(l) {policyMeet.insert(.hasUppercase)}
        if nb.contains(l) {policyMeet.insert(.hasNumber)}
        if sb.contains(l) {policyMeet.insert(.hasSymbol)}
    }
}

var body: some View {
    VStack(){
            TextField("Enter Password", text: $password)
                .textFieldStyle(PlainTextFieldStyle())
                .padding(5)
                .disableAutocorrection(true)
                .autocapitalization(.none)
                .onChange(of: password) { newPassword in
                    checkPassword()
                }
            TextField("Re-enter Password", text: $confirmPassword)
                .textFieldStyle(PlainTextFieldStyle())
                .padding(5)
                .disableAutocorrection(true)
                .autocapitalization(.none)
                ...
    }
}

Adding the confirm password indicator

We need one line of text to indicate if the confirm password is same as the password. This can be simply done by adding a HStack with an Image and a Text after the second TextField

HStack() {
            Image(systemName: "checkmark.circle")
                .font(.footnote)
                .foregroundColor(.green)
                .opacity((password.count > 0 && password == confirmPassword) ? 1.0 : 0.0)
            Text("Password match")
                .font(.footnote)
                .foregroundColor((password.count > 0 && password == confirmPassword) ? .green : .red)
                .opacity((password.count > 0 && password == confirmPassword) ? 1.0 : 0.0)
        }

Let’s take a spin of our view


Change the TextField to SecureField

It is more often that the password input field is a secure field. But for the setup view, sometimes it is helpful if the user can see what they typed. We can easily add a button that toggles between TextField and SecureField. The button’s action will toggle a boolean @State variable and in our view build code, we use TextField or SecureField according to that variable’s value. Both TextField and SecureField support all the modifiers we need so it is an easy switch.

But rather than adding the same logic twice, let’s first extract the password input field into a separate view. The first input field need .onChange(of:) modifier but not the second input field. We can make the action an optional function variable in the new view and invoke it in the .onChange(of:) modifier if the action is not nil.

struct PasswordInputField: View {

    var label: String = ""
    @Binding var text: String

    var onChange: ((String) -> Void)? = nil

    var body: some View {

        TextField(label, text: $text)
            .textFieldStyle(PlainTextFieldStyle())
            .padding(5)
            .disableAutocorrection(true)
            .autocapitalization(.none)
            .onChange(of: text) { newText in
                if onChange != nil {onChange!(newText)}
            }

    }
}

Then we changed our callside to use the new PasswordInputField view, one with the onChange function, one without.

PasswordInputField(label: "Enter Password", text: $password, onChange: { _ in
            checkPassword()
        })
...
PasswordInputField(label: "Re-Enter Password", text: $confirmPassword)

Now we can focus on the PasswordInputField. Put an Button with an eye icon into a HStack with the TextField. Add a @State variable called isSecureField, and assign true as the default value. In the action of the Button, toggle that isSecureField. And use it to control which icon to show on the Button as well. Lastly, embed an if-else in the HStack to create TextField or SecureField base on the value of isSecureField.

struct PasswordInputField: View {

    @State private var isSecureField: Bool = true

    var label: String = ""
    @Binding var text: String

    var onChange: ((String) -> Void)? = nil

    var body: some View {
        HStack() {
            if isSecureField {
                SecureField(label, text: $text)
                    .textFieldStyle(PlainTextFieldStyle())
                    .padding(5)
                    .disableAutocorrection(true)
                    .autocapitalization(.none)
                    .onChange(of: text) { newText in
                        if onChange != nil {onChange!(newText)}
                    }
            } else {
                TextField(label, text: $text)
                    .textFieldStyle(PlainTextFieldStyle())
                    .padding(5)
                    .disableAutocorrection(true)
                    .autocapitalization(.none)
                    .onChange(of: text) { newText in
                        if onChange != nil {onChange!(newText)}
                    }
            }

            Button(action: {
                isSecureField.toggle()
            }) {
                Image(systemName: isSecureField ? "eye" : "eye.slash")
                    .font(.title3)
                    .foregroundColor(Color.blue)
            }
        }
    }
}

The final view

Tada! Now we have the fully functional password setup view built in SwiftUI. You can add your tweak in the layout or color or icon to make it looks better. But all the functionalities are there!

The complete code can be found here. Deploy target iOS 14.0, Xcode 12.5.1