Creating views for failed and successful user authentication

Designing the view for failed and successful user authentication ✍️↔️

A basic approach in SwiftUI is to set up the interface individually for every relevant situation that can occur during the app’s runtime. In our example, there are three scenarios that can occur:

  • Scenario 1: The user did nothing but entering his credentials into the TextFields. Especially, he didn’t start the authentication process by tapping on the login button. In this situation, we can use the ContentView we created as it is and don’t need to modify anything.
  • Scenario 2: The user entered the authentication process by tapping on the login button and it failed. In this case, we want to display a Text view saying that the entered information was wrong.
  • Scenario 3: The user entered the authentication process by tapping on the login button and it succeeded. In this case, we want to display a text saying that the login was successful.
How our ContentView should look for Case 1, Case 2 and Case 3

Handling the second and the third scenario is done by using State properties again. Let’s start by handling the second case, meaning that the authentication process did fail. For this purpose, we declare a Boolean-type State property and assign it to false by default.

struct ContentView: View {
    
    @State var username = ""
    @State var password = ""
    
    @State var authenticationDidFail = false
    
    var body: some View {
        VStack {
            //...
    }
}

The authenticationDidFail State getting assigned to true means that the authentication did fail. When this happens, we want to display a Text view between the SecureField and the Login-Button. Therefore, we write:

VStack {
    //...
    PasswordSecureField(password: $password)
    if authenticationDidFail {
        Text("Entered credentials incorrect. Try again.")
            .padding(.bottom, 15)
            .foregroundColor(.red)
    }
    Button(action: {
        print("Login Button tapped.")
    }) {
        LoginButtonContent()
    }
}

Let’s take a look at the Preview simulator:  Where is our created Text view? Because we set the value of the authenticationDidFail State to false, we currently don’t simulate the case that the authentication process did fail. Let’s change this:

@State var authenticationDidFail = false

Now we are simulating a failed authentication process. Note how the new Text view pops up in the preview simulator:

Perfect, we styled our ContentView for communicating a failed authentication process to the user (Scenario 2). Let’s set the authenticationDidFail State to false again to simulate our default ContentView (Scenario 1).

Let’s repeat this workflow for a successful authentication process (Scenario 3). Again, we need to declare a State property for handling this situation. Let’s assign it to true to simulate how the view should look if the login process succeeds:

@State var authenticationDidFail = false
@State var authenticationDidSucceed = true

If it succeeds, we want another Text view to pop up saying that the login process succeeded. But we don’t want this Text to be embedded within the VStack but to be placed on top of it. To achieve this, wrap the VStack itself into a ZStack. Remember, all views inside a ZStack get stacked on top of each other.

ZStack {
    VStack {
        //...
    }
        .padding()
}

Let’s add a Text view reading “Login succeeded” to the ZStack by inserting it below the VStack. 

ZStack {
    VStack {
        //...
    }
        .padding()
    if authenticationDidSucceed {
        Text("Login successful!")
    }
}

Great, the Text view gets stacked on top of our VStack.

Let’s modify this Text by styling the font and adding a green background to it. Also, add a .animation modifier with the default animation type. This modifier causes the Text to pop up with a decent animation as we will see later.

if authenticationDidSucceed {
    Text("Login successful!")
        .font(.headline)
        .frame(width: 250, height: 80)
        .background(Color.green)
        .cornerRadius(20)
        .foregroundColor(.white)
        .animation(Animation.default)
}

This is what your Preview simulator should show so far:

We are done with handling our third scenario, so let’s set the authenticationDidSucceed State to false again for simulating our default scenario 1.

@State var authenticationDidSucceed = false

Great! We prepared our ContentView for the different scenarios that can occur when the user interacts with our app. Let’s start implementing our code’s logic for toggling these States appropriately.

Implementing the logic for toggling the States 🤓

So, we just defined how our ContentView should look depending on the different State Booleans. But how do we toggle these States?

Pretty simple: When the user taps on the Login-Button, we need to compare the entered username and password with the stored credentials. 

We do not have stored the correct password and username yet. Let’s change this by inserting them somewhere outside our ContentView struct, e.g. above it.

let storedUsername = "Myusername"
let storedPassword = "Mypassword"

If the entered information is correct we want to set the authenticationDidSucceed property to true which then causes the ContentView to refresh its body with eventually showing the “Login succeeded” Text view. Let’s write this logic into the action parameter of our Button:

Button(action: {
    if username == storedUsername && password == storedPassword {
        authenticationDidSucceed = true
    }
}) {
    LoginButtonContent()
}

If the entered information doesn’t equal the stored, we set the authenticationDidFail State to true.

if username == storedUsername && password == storedPassword {
    authenticationDidSucceed = true
} else {
    authenticationDidFail = true
}

That’s all! Let’s try it out. Run your app and try to enter the wrong credentials and tap on the login button. What happens is that the code inside the action closure checks if the credentials are correct. Because that is not the case, it sets the authenticationDidFail State to true. The view, therefore, refreshes with eventually showing the “Information not correct” Text view.

If you enter the right credentials, the authenticationDidSucceed State gets toggled which causes the ContentView to show the “Login succeeded” Text view. Note how the text pops up with a smooth animation. This is the one we defined earlier by applying the .animation modifier to the corresponding Text view.

Maybe you noticed that when entering the wrong credentials first and then entering the correct username and password both Text views are shown at the same time: The Text view reading that the entered credentials were wrong and the “Login succeeded” Text. 

But we don’t want to see them both. Instead, we want the warning Text to disappear when the login succeeded. We can simply implement this by updating our action:

Button(action: {
    if username == storedUsername && password == storedPassword {
        authenticationDidSucceed = true
        authenticationDidFail = false
    } else {
        authenticationDidFail = true
    }
}) {
    LoginButtonContent()
}

This makes sure that the authenticationDidFail State is false when the credentials are correct, which in turn ensures that the disclaimer text is not shown when the successful login text is.

Let’s run our app again, enter some wrong credentials and tap the Login-Button. After seeing the “Information not correct” Text, enter the right credentials and tap the Login-Button again. See how the “Information not correct” Text disappears and how the “Login succeeded” Text shows up instead!

We can also reset the SecureField once the user tries to log in with incorrect credentials by assigning an empty String to the password State again.

Button(action: {
    if username == storedUsername && password == storedPassword {
        authenticationDidSucceed = true
        authenticationDidFail = false
    } else {
        authenticationDidFail = true
        password = String
    }
}) {
    LoginButtonContent()
}

Finally, we want to automatically try to log in once the user taps on the returns button of the toggled keyboard after entering the password. To do this, let’s outsource the code inside the Button’s action into its own function below the ContentView‘s body.

Button(action: {
    tryToLogin()
}) {
    LoginButtonContent()
}
func tryToLogIn() {
    if username == storedUsername && password == storedPassword {
        authenticationDidSucceed = true
        authenticationDidFail = false
    } else {
        authenticationDidFail = true
        password = ""
    }
}

Finally, add the .onSubmit modifier to the PasswordSecureField and call the tryToLogIn function.

PasswordSecureField(password: $password)
    .onSubmit {
        tryToLogIn()
    }

Let’s run our app again to see if everything works as expected:

Conclusion 🎊

That’s it! We are finished creating our login page app with SwiftUI. You learned a lot of stuff for example how to use States and Bindings and how data flow and input in SwiftUI works.

In the next chapter, we are going to meet some more control views than Buttons and TextFields by extending our Food Delivery App with an order form.

5 replies on “Creating views for failed and successful user authentication”

I really like how you’re emphasizing ‘outsourcing’ the views into separate structures. This makes the code so much easier to understand and maintain in the future. Not to mention it opens the door to easily reusing the extracted views in other parts of the app.

I think the interface would look complete if when login succeed button appears, the background is blurred or covered by grey so that the user can focus on the pop up alert

How to do so that after one of the validation messages appears, the screen is cleaned and only the last message remains ?

Leave a Reply

Your email address will not be published. Required fields are marked *