How to build a login page in SwiftUI #2 – Outsource bindings and implement State-depending views

Share on facebook
Share on twitter
Share on pinterest

This is the second of two parts of our tutorial on how to build a login page just with SwiftUI, for the first one click here.

Welcome to this two-part tutorial series on how to create a full-functional login page by just using SwiftUI. In the last part, we talked about implementing Text Views and learn the basics concept of data flow in SwiftUI by using State properties. In this part, we will extend this knowledge by learning how to create a binding between different views and how to show views dynamically.

Here is what we are going to create:

What you will learn 💡

What you will learn in this part of the tutorial:

  • How to outsource views that are bound to State properties
  • How to represent several views depending on the current State
  • How to move up the view’s content when the keyboard is toggled

How to outsource views depending on a @State property

As said in the last part of this tutorial, outsourcing views that are bound to a State, like our Text Fields are, is a little bit special. Let’s try out to outsource our username Text Field by CMD-clicking on it and selecting Extract subview. Lets call it UsernameTextField.

We get an error saying “Use of unresolved identifier ‘$username'”. This is because when outsourcing a view, it gets wrapped into its own struct. But the UsernameTextField struct doesn’t own such a property which can get bind to the TextField object. To fix this, we need to insert an appropriate property into our outsourced UsernameTextField struct.

But: Instead of creating a State property on its own we want to instead have a property that derives its content from the username State of our ContentView in order to enable data flow between them! To do this, we declare a @Binding property that’s also called username above our UsernameTextField’s body and since we want to derive its content from the username State of the ContentView we don’t assign any data to it. Bindings are used for creating a data connection between a view (here: our outsourced UsernameTextField) and its source of data (here: our username State)

“Use a binding to create a two-way connection between a view and its underlying model.”

Apple
struct UsernameTextField : View {
@Binding var username: String
var body: some View {
return TextField("Username", text: $username)
            .padding()
            .background(lightGreyColor)
            .cornerRadius(5.0)
            .padding(.bottom, 20)
    }
}

To create the “link” between our ContentView and our UsernameTextField we initialise the latter by passing the username State of our UsernameTextField into it! As we did in the last part of this tutorial, we use the dollar-sign syntax again for binding objects.

struct ContentView : View {
@State var username: String = ""
@State var password: String = ""
var body: some View {
        VStack {
WelcomeText()
UserImage()
UsernameTextField(username: $username)
SecureField("Password", text: $password)
                .padding()
                .background(lightGreyColor)
                .cornerRadius(5.0)
                .padding(.bottom, 20)
Button(action: {print("Button tapped")}) {
LoginButtonContent()
            }
        }
        .padding()
    }
}

Now our outsourced UsernameTextField is connected to the username State of our ContentView!

What happens now when the user enters something is that the entered character gets passed from the UsernameTextField to the username property of the outsourced view which serves as a “bridge” and passes the data itself through the binding to the username State of the ContentView.

As learned in the last part the whole view gets then rerendered. The TextField object reads the content of the username State of the ContentView via accessing the username Binding of the UsernameTextField view and using it as a “data bridge”.

Let’s repeat this process for the Secure Field!

1. Outsource the Secure Field by CMD-clicking on it and selecting extract subview. Let’s call it PasswordSecureField.

2. Insert a Binding property without any content into the extracted PasswordSecureField.

struct PasswordSecureField : View {
    
    @Binding var password: String
    
    var body: some View {
        return SecureField("Password", text: $password)
            .padding()
            .background(lightGreyColor)
            .cornerRadius(5.0)
            .padding(.bottom, 20)
    }
}

3. Link the outsourced PasswordSecureField to the State of the ContentView by initialising it inside the ContentView with binding the password State to it.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            UsernameTextField(username: $username)
            PasswordSecureField(password: $password)
            Button(action: {print("Button tapped")}) {
               LoginButtonContent()
            }
        }
        .padding()
    }
}

Great, we finished outsourcing our Text and Secure Field! Let’s go further with designing our view for a failed and successful 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 cases that can occur:

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

How our view should look for Case 1, Case 2 and Case 3

Handling the second and the third case is done by using State properties as well!

Let’s start by handling the second case, meaning that the authentication process did fail. For this purpose, we declare a State property of a boolean type and set its default value to false.

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    @State var authenticationDidFail: Bool = false
    
    var body: some View {
        
        //[...]
      
    }
}

When the State’s value becomes true that means that the authentication did fail. If this is the case we want to insert a Text object between the password Secure Field and the login Button. Therefore we write:

struct ContentView : View {
    
    @State var username: String = ""
    @State var password: String = ""
    
    @State var authenticationDidFail: Bool = false
    
    var body: some View {
        
        VStack {
            WelcomeText()
            UserImage()
            UsernameTextField(username: $username)
            PasswordSecureField(password: $password)
            if authenticationDidFail {
                Text("Information not correct. Try again.")
            }
            Button(action: {print("Button tapped")}) {
               LoginButtonContent()
            }
        }
        .padding()
    }
}

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

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = true
    
    var body: some View {
        
        //[...]
        
    }
}

Now we are simulating a failed authentication and therefore see our inserted Text object! Let’s give it a red color and move it a little bit up:

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        VStack {
            //[...]
            if authenticationDidFail {
                Text("Information not correct. Try again.")
                    .offset(y: -10)
                    .foregroundColor(.red)
            }
            //[...]
    }
}

This is how your Preview Simulator should look like now:

Perfect, we styled our view for a failed authentication (our Case 2). Let’s set the authenticationDidFail State to false again to simulate our default view (Case 1).

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

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = false
    @State var authenticationDidSucceed: Bool = true
    
    var body: some View {
        
        //[...]
        
    }
}

If it succeeds, we want a Text to pop out saying that the login succeeded! But we don’t want this text to be within the VStack but on top of it! How do we achieve this?

Fortunately, it’s really straight forward. Just wrap the VStack itself into a ZStack. All objects inside a ZStack gets stacked on top of each other.

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                WelcomeText()
                UserImage()
                UsernameTextField(username: $username)
                PasswordSecureField(password: $password)
                if authenticationDidFail {
                    Text("Information not correct. Try again.")
                        .offset(y: -10)
                        .foregroundColor(.red)
                }
                Button(action: {print("Button tapped")}) {
                    LoginButtonContent()
                }
                }
                .padding()
        }
    }
}

All objects inside a ZStack get stacked on top of each other.

“ZStack: A view that overlays its children, aligning them in both axes.”

Apple

So let’s add a Text object saying “Login succeeded” to the ZStack by inserting it below the VStack.

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
            if authenticationDidSucceed {
                Text("Login succeeded!")
            }
        }
    }
}

You now see that the Text is positioned on top of our VStack.

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

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
            if authenticationDidSucceed {
                Text("Login succeeded!")
                    .font(.headline)
                    .frame(width: 250, height: 80)
                    .background(Color.green)
                    .cornerRadius(20.0)
                    .foregroundColor(.white)
                    .animation(Animation.default)
            }
        }
    }
}

This is what your preview should look like now:

We are done with handling our third case, so lets set the authenticationDidSucceed State to false again for simulating our default case!

struct ContentView : View {
    
    //[...]
    
    @State var authenticationDidFail: Bool = false
    @State var authenticationDidSucceed: Bool = false
    
    var body: some View {
        
        //[...]
        
    }
}

Great! We are done view designing our views that should depend on which State is currently “active”. Let’s hop into implementing our code logic for toggling these States appropriately.

Implementing the logic for toggling the States 🤓

So we just defined how our view should look depending on the different States. But how do we toggle these States?

Pretty simple: When the user taps 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 below our lightGreyColor constant

import SwiftUI

let lightGreyColor = Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0)

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

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        //[...]
    }
}

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

struct ContentView : View {
    
    //[...]
    
    var body: some View {
        
        ZStack {
            VStack {
                //[...]
                Button(action: {
                    if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                    }
                }) {
                    LoginButtonContent()
                }
                }
                .padding()
            //[...]
        }
    }
}

If the entered information is not the same as the stored, we want to instead set the authenticationDidFail State to true!

struct ContentView : View {
//[...]
    
var body: some View {
        ZStack {
            VStack {
//[...]
                Button(action: {
if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                    } else {
                        self.authenticationDidFail = true
                    }
                }) {
LoginButtonContent()
                }
                }
                .padding()
//[...]
        }
    }
}

That’s all! Let’s try it out. Run your app in the “normal simulator”. Try to enter the wrong credentials and hit login! What happens now, is that the code inside the action checks if the credentials are correct. Because that is not the case, it toggles the authenticationDidFail State to be true. The view, therefore, rerenders with showing the “Information not correct” Text.

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

Maybe you noticed, that when first entering the wrong and then the right information we see both: The disclaimer that the entered information was wrong and the “Login succeeded” Text.

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

struct ContentView : View {
//[...]
    
var body: some View {
        ZStack {
            VStack {
//[...]
                Button(action: {
if self.username == storedUsername && self.password == storedPassword {
                        self.authenticationDidSucceed = true
                        self.authenticationDidFail = false
                    } else {
                        self.authenticationDidFail = true
                    }
                }) {
LoginButtonContent()
                }
                }
                .padding()
//[...]
        }
    }
}

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

Lets rerun our app, write down wrong credentials and tap the login button. After seeing the “Information not correct” Text enter the right credentials and hit the login button again. See how the “Information not correct” Text disappears and how the “Login succeeded” Text shows up instead!

Prevent the keyboard from hiding the Text View

There is one last thing we have to talk about in order to provide a good user experience: When the user taps on a Text Field and enters his password or username, the toggled keyboard hides the Text Field. This causes the user to not be able to see what he enters, which can be pretty annoying.

We outsourced this topic! To learn how to prevent the keyboard from hiding your view’s content, take a look at this tutorial.

Conclusion 🎊

That’s it! We are finished with creating our login page app in SwiftUI. You learned a lot stuff like using States, Binding and how data flow in SwiftUI works. How to manage outsourced views and data input and little tweaks like how to move content up when the user is writing something!

I uploaded the whole source code to GitHub if you want to check it out!

I hope you enjoyed this tutorial. Make sure you subscribe to our newsletter to not miss any updates, a lot of stuff about SwiftUI is coming. Also visit us on Facebook and Instagram. If you have any questions do not hesitate to contact us or leave a comment below!

3 replies on “How to build a login page in SwiftUI #2 – Outsource bindings and implement State-depending views”

Leave a Reply

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

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book