Forms made easy with SwiftUI

In this post, we will build a registration form with SwiftUI.

Let's start by creating a new Xcode Project : Choose a single view app, and dont forget to check "Use SwiftUI".

Xcode creates a ContentView for you, we'll work with this struct as our main view. First, we will add our registration fields values. Let's say name, email, and password. We need to use the property wrapper @State for our values. If you don't know what @State property wrapper is, I highly recommande to read my post about Properties Wrappers.
This is what we should have by now :

struct ContentView : View {

    @State private var name: String = ""
    @State private var email: String = ""
    @State private var password: String = ""

    var body: some View {
        Text("Hello World!")
    }
}

Now in our view, we want to make a NavigationView that will contain a Form.

struct ContentView : View {

    @State private var name: String = ""
    @State private var email: String = ""
    @State private var password: String = ""

    var body: some View {
        NavigationView {
            Form {
                Text("Hello World!")
            }
            .navigationBarTitle(Text("Registration Form"))
        }
    }
}

By now, if you build and run you should see this :

As you can see, the form object creates a List and every View in it will be a cell, just like our text here, Hello World!.

In this form, we will remove the Hello World text and add instead some sections, three sections actually. One that we will use for the name and mail text fields, another for the password and the last one for our call to action validation button.
A Section takes 3 parameters : a header, a footer and the content (View). Here, we will set headers to our 2 first sections :

struct ContentView : View {

    @State private var name: String = ""
    @State private var email: String = ""
    @State private var password: String = ""

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Your Info")) {
                    TextField($name, placeholder: Text("Name"))
                    TextField($email, placeholder: Text("Email"))
                }
                Section(header: Text("Password")) {
                    TextField($password, placeholder: Text("Password"))
                }
                Section {
                    Button(action: {
                                print("register account")
                            }) {
                                Text("OK")
                            }
                }
            }
            .navigationBarTitle(Text("Registration Form"))
        }
    }
}

Which would result into that :

That's it. We now have a simple form made with SwiftUI. Note that we passe the fields values with a $ so that we have a 2 ways binding : the textfields read and write the value we pass to them.

Now, let's try to add a few more things to this...
I would like to add an indicator for the security level of the password.

First, we are going to create an enum for the password security level :

enum PasswordLevel: Int {
    case none = 0
    case weak = 1
    case ok = 2
    case strong = 3
}

And a view :

struct SecureLevelView : View {
    var level: PasswordLevel
    var body: some View {
        HStack {
            RoundedRectangle(cornerRadius: 8).foregroundColor(self.getColors()[0]).frame(height: 10)
            RoundedRectangle(cornerRadius: 8).foregroundColor(self.getColors()[1]).frame(height: 10)
            RoundedRectangle(cornerRadius: 8).foregroundColor(self.getColors()[2]).frame(height: 10)
        }
    }

    func getColors() -> [Color] {
        switch self.level {
        case .none:
            return [.clear, .clear, .clear]
        case .weak:
            return [.red, .clear, .clear]
        case .ok:
            return [.red, .orange, .clear]
        case .strong:
            return [.red, .orange, .green]
        }
    }
}

We can init our view with a level and depending on the level it will display the right colors like so :

From the top to the bottom : .none (no colors), .weak, .ok, .strong.

Now instead of having a simple value for the password, we will make a class that will handle holding and checking the password.
We need this class to be bindable so it can notify the view for the changes of the protection level.

class PasswordChecker: BindableObject {
    public let didChange = PassthroughSubject<PasswordChecker, Never>()
    var password: String = "" {
        didSet {
            self.checkForPassword(password: self.password)
        }
    }

    var level: PasswordLevel = .none {
        didSet {
            self.didChange.send(self)
        }
    }

    func checkForPassword(password: String) {
        if password.count == 0 {
            self.level = .none
        } else if password.count < 2 {
            self.level = .weak
        } else if password.count < 6 {
            self.level = .ok
        } else {
            self.level = .strong
        }
    }
}

Don't forget to import Combine.

The class has 2 attributes : the password and the level.
It also has a didChange property to conform to BindableObject. I you don't know what a BindableObject is, here is the post where I explain what this is and how it works.
What we want here is to update the password level on the didSet of the password, so that when the password's value is updated, we set the level according to the setted password. When the level is setted, we want to notify the view that the password level has changed so it can update the SecureLevelView.

So here, in the checkForPassword method, I choose to make simple rules : the protection level depends on the number of characters in the password, but you might need to use actual rules, checking on capitalized letters, numbers or special characters with a regexp.

Now we want to use that class in our view, we just need to remove the password property and replace it with a PasswordChecker.

struct ContentView : View {

    @State private var name: String = ""
    @State private var email: String = ""
    @ObjectBinding var passwordChecker: PasswordChecker = PasswordChecker()

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Your Info")) {
                    TextField($name, placeholder: Text("Name"))
                    TextField($email, placeholder: Text("Email"))
                }
                Section(header: Text("Password")) {
                    TextField($passwordChecker.password, placeholder: Text("Password"))
                    if !self.passwordChecker.password.isEmpty {
                        SecureLevelView(level: self.passwordChecker.level)
                    }
                }
                Section {
                    Button(action: {
                                print("register account")
                            }) {
                                Text("OK")
                            }
                }
            }
            .navigationBarTitle(Text("Registration Form"))
        }
    }
}

I chose to hide the SecureLevelView when the password is empty.

Now this is what we have :

The last feature I'd like to have is a toggle to make the user accepts the terms and conditions.
I added a new @State Boolean property :

@State private var terms: Bool = false

and in the section :

                    if self.passwordChecker.level.rawValue >= 2 {
                        Toggle(isOn: $terms) {
                            Text("Accept the terms and conditions")
                        }
                        if self.terms {
                            Button(action: {
                                print("register account")
                            }) {
                                Text("OK")
                            }
                        }
                    }

Meaning : if the password protection level is higher than ok (ok or strong), we show the "Accept the terms and conditions" Toggle and only if the toggle is on, then show the validation button.

I agree, this is a terrible UX. But now you know how to build easy forms and dynamically show info as user types in, using combine.

Hope you enjoyed this little post. You can download the full source here on my github.

Happy coding !

Étiquettes :

Leave a Comment