Understanding @State and mastering TextFields

Let’s start with the first one of our two TextFields. TextFields allow the user to enter their credentials to log into his account. To create the first one, insert a TextField view into the VStack right below the UserImage. We need to provide our TextField with two parameters: the text and the (unnamed) String parameter.

VStack {
    WelcomeText()
    UserImage()
    TextField( , text: )
}

Creating a TextField without feeding it with the necessary parameters causes errors. We will get rid of these in a moment. But first of all, let’s talk about these parameters:

The (unnamed) String parameter provides us with a placeholder when the TextField is empty. The text parameter accepts a binding. What does this mean? A binding is a reference to a State that the TextField can use to store the information that the user enters. The State then processes the information and in turn displays it to the user.

Let’s create such a State our TextField can bind to. Let’s declare a variable above our ContentView’s body named username which accepts a String and mark it with the @State keyword. By default, i.e. when the user has not yet entered anything, this variable should contain an empty String.

struct ContentView: View {
    
    @State var username = ""
    
    var body: some View {
        VStack {
            WelcomeText()
            UserImage()
            TextField( , text: )
        }
    }
}

Let’s take a more detailed look at how this link between the TextField and its bound State works.

The TextField will be connected to the username State property which is currently assigned to an empty String. When the user enters a character, for example, the letter “A”, this character gets assigned to the @State property that is bound to the TextField. The update of the data assigned to the State causes, as you learned in the last chapter, the view to rebuild its body including the TextField. When the TextField view gets updated it reads the new String assigned to the username State, which is now “A”. This is how the user eventually sees what he has entered!

Whenever the State gets updated, it causes the related view to rerender its body. Every time the view rerenders, it reads out the new value assigned to the State

Back to our TextField view: Since we created an appropriate State property, we can bind it to our TextField. To bind objects to a State property, we have to insert the property by using the following dollar-sign syntax:

TextField( , text: $username)

When the TextField view is empty (because the bound State is assigned to an empty String), we don’t want the TextField to display nothing. Instead, we want the TextField to display a placeholder. We can do this by passing a String to the TextField view like this:

TextField("Username", text: $username)

Currently, the TextField view doesn’t have any border or background. To change this, we have to add some modifiers to it. At this point, we use the LightGray color set we created earlier for the TextField’s background. We also add a .padding modifier again for extending the space between this TextField and the one we will create in a moment.

TextField("Username", text: $username)
    .padding()
    .background(Color("LightGray"))
    .cornerRadius(5)
    .padding(.bottom, 20)

Great! We’re finished with setting up our first TextField. We want to outsource the TextField as well. However, outsourcing views that are bound to a State is somewhat tricky. We’ll learn how to do this in a moment.

First, we want to add another view for letting the user enter his password. We could use a TextField view again, but we want to provide our login form with a little bit of privacy. We want to do this by changing the field’s content to dots when the user enters his password.

Doing this is really simple: Instead of inserting a TextField again, we insert a similar view called SecureField. The SecureField gets initialized with the same arguments as a TextField. Therefore, we create another @State property named password and bind it to the SecureField. We also insert an appropriate placeholder. For styling the SecureField like the TextField, just copy & paste the modifiers of the TextField to the SecureField.

VStack {
    //...
    SecureField("Password", text: $password)
        .padding()
        .background(Color("LightGray"))
        .cornerRadius(5)
        .padding(.bottom, 20)
}

Next, we add some overall padding to the VStack itself since we don’t want the TextField and SecureField to touch the edges of the device’s screen.

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

Here’s what your ContentView preview should show so far:

Creating the Login button 🛠

Last but not least, let’s add the Login-Button. As you already learned, to create a Button, we start with creating the view it contains. This should be a Text view reading “LOGIN”. To modify the Text View’s style and add a green background to it, apply the following modifiers to it:

VStack {
    //...
    Text("LOGIN")
        .font(.headline)
        .foregroundColor(.white)
        .padding(10)
        .frame(width: 220, height: 60)
        .background(Color.green)
        .cornerRadius(15.0)
}
    .padding()

Next, outsource the Text view and name it LoginButtonContent. Then, wrap the LoginButtonContent view into a Button view. The Button view needs to know what to do when the user taps on it. For now, we just insert a dummy print statement. We will implement our authentication logic in a moment.

Button(action: {
    print("Login Button tapped.")
}) {
    LoginButtonContent()
}

Until the user entered at least one character into the TextField and SecureField we want to disable the Button and lower its opacity. We can achieve this by writing:

Button(action: {
    tryToLogin()
}) {
    LoginButtonContent()
}
    .disabled(!username.isEmpty && !password.isEmpty ? false : true)
    .opacity(!username.isEmpty && !password.isEmpty ? 1 : 0.4)

This means: If the String assigned to the username and the String assigned to the password is not empty do not disable the Button and use the full opacity. Otherwise, disable the Button and lower the opacity to 40%.

Let’s run our app to check if the TextField and SecureField are working.

To check out the full functionality, run the app in the regular simulator, not the preview simulator. We can now use the TextField and SecureField to type in the user’s credentials. Note how the password gets invisible when being entered.

Hint: If you want to use the simulator keyboard and it doesn’t open automatically when clicking on the TextFields, click on the simulator and choose I/O -> Keyboard -> Toggle Software Keyboard in the macOS menubar. If you then click on a TextField, the keyboard should open automatically. You can also manually toggle the Software Keyboard by pressing “CMD-K”.

This is a great checkpoint to remember how the State data flow works: if you enter something, the entered character gets assigned to the @State property. Because the State’s content gets updated, it automatically triggers the related ContentView to render its body. Then, the TextField reads out the State’s content again and displays the entered character to the user. This cycle repeats every time the user enters something.

Outsourcing the Text Fields

As mentioned before, outsourcing views that are bound to a State, like our Text Fields, is somewhat tricky. Let’s try to outsource our username TextField by CMD-clicking on it and selecting “Extract Subview”. Let’s name it UsernameTextField.

We get an error saying “Use of unresolved identifier ‘$username'” or “Cannot find $username in scope”. This is because when outsourcing a view, it gets wrapped into its own struct. However, the UsernameTextField struct doesn’t own such a property that can get bound to the TextField view.

To fix this, we need to declare a corresponding property in our outsourced UsernameTextField struct.

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

struct UsernameTextField: View {
    
    @Binding var username: String
    
    var body: some View {
        TextField("Username", text: $username)
            //...
    }
}

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

UsernameTextField(username: $username)

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 already learned, the whole ContentView then gets refreshed (including the UserTextField instance it contains). The TextField view reads out the content assigned to the username State of the ContentView through 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 SecureField by CMD-clicking on it and selecting “Extract Subview”. Let’s name it “PasswordSecureField”.

2. Insert a Binding property without any data assigned to it into the extracted PasswordSecureField.

struct PasswordSecureField: View {
    
    @Binding var password: String
    
    var body: some View {
        SecureField("Password", text: $password)
            //...
    }
}

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

PasswordSecureField(password: $password)

Great, we finished outsourcing our TextField and SecureField!

Recap 🎊

We already completed the UI of our login page.

You just learned how to create more complex interfaces in SwiftUI and you got familiar with two important views, TextFields and SecureFields. You can use those to handle user input. We also learned how to style and outsource them properly.

Let’s move on with designing our view for a failed and successful authentication!

Leave a Reply

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