In the last part of this SwiftUI course, you've learned all about how to create and arrange SwiftUI views to craft beautiful interfaces. In this part, you'll put those skills to test by building out a login screen for your app.
You'll build a login screen with an email field, some information and a button at the bottom. Besides just building the screen, we'll also add a way to navigate from the welcome screen you created in the last part to the new login screen.
Ready to get started?
You can find the finished project code on GitHub.
Kicking things off
To create the login screen, create a new SwiftUI View file called LoginView.swift. For now, change the body to display a text saying "Log In":
You'll fill up this placeholder screen later, but you'll first need a way to navigate to the screen.
Navigation in SwiftUI
Two SwiftUI views will help you with navigation: NavigationView and NavigationLink. NavigationView is SwiftUI's version of UINavigationController: It manages a stack of views and shows a navigation bar on top of them.
NavigationLink is similar to a Button, but it triggers presenting a new view in the navigation stack when it's pressed. To work correctly, a NavigationLink needs to be nested inside a NavigationView. In UIKit terms, if NavigationView is the navigation controller, NavigationLink is a segue.
Adding a SwiftUI Navigation View and a navigation bar
In iOS apps, you'd often have one navigation controller that will be the initial view controller of your app. You can achieve a similar effect in SwiftUI by making the initial view a NavigationView.
The initial view is determined by the SceneDelegate. If you look inside SceneDelegate.swift you'll find an implementation of scene(_:willConnectTo:, options:) that creates a content view and adds it to the window.
Instead of creating a plain WelcomeView, change contentView so that you wrap the welcome screen in a navigation view:
contentView, just like any other SwiftUI view, can be arbitrarily complex. The scene delegate is a good place to add global configuration, like changing colors, adding routing, subscribing to global events and other changes that affect the whole app.
Run the project now and you'll notice a giant space on the top of your welcome screen.
This space is taken up by an empty navigation bar. Let's look at how to change the look and content of this bar.
Styling the navigation bar in SwiftUI
Let's fill up the navigation bar with a title on the welcome screen. Open WelcomeView.swift, and add a navigation bar title to the top-most VStack:
If you run the project, you might notice there's now two texts saying "Create an account".
Remove the one inside the VStack:
The view looks the same as it did before, but the text is now part of the navigation bar.
Note: If you're wondering how to get the default-looking iOS navigation bar, you can use navigationBarTitle(_:displayMode:) and pass in .inline as the display mode.
Since your view is wrapped inside a NavigationView, you can now use NavigationLink to present the login screen. Wrap the primary button in body inside a NavigationLink (instead of a Button):
Just like Button, NavigationLink will wrap around a view and make it interactable. Instead of calling a function, though, the navigation link will present the provided view when it's tapped.
If you run the project now and tap the log in button, you'll be taken to your login view.
The view inside the navigation link can be anything you want, but be aware that views that respond to touches, like buttons, will consume the touch and it won't propagate to the navigation link. In other words, buttons inside navigation links won't trigger the presentation. Watch out for those hungry buttons, lest they eat your touches!
Making a reusable SwiftUI text field
Now that you can navigate to the login view, you'll start building out that view.
Since you're building a login form, you'll first build a reusable text field that you can use throughout your app. Small reusable views are a leitmotif of SwiftUI apps! Once you're done, it will look like this:
Already you can see the views you'll combine to create this text field. In a vertical stack, you'll need Text for the title, a TextField to edit the text, an Image to show the icon and a way to display the line on the bottom of the view.
Before you start working on the view, let's first add the little email icon. You can find the image here. Drag it over to your Assets.xcassets file and name it email.
Next, create a new SwiftUI View file called ErrorTextField.swift. It's called error text field because you'll add the ability to show an error if the text is invalid. Change the struct to the following:
It looks like a lot of code but don't worry — it's a boilerplate initializer that sets up all the necessary properties of the view. You'll need a title and a placeholder, the image name of the icon, a function to validate the text and the keyboard type of the text field.
You'll also need a binding to the text. A binding is similar to a state variable, but it's used to bind data between two different views. For instance, you can provide the text field with a binding to your text. The text field will change the text, and your view will be re-drawn.
Think of binding as a state variable that you can pass to other views. It's kind of like giving someone your phone number and telling them to call you when something changes.
In this case, you'll receive a binding to the text that you'll pass along to the TextField view.
Since this view can show an error, add a computed property that will determine whether an error should be shown:
There's no point in showing the error if the text is empty. If the text is not empty, you'll use the provided text validation function to determine if an error should get shown.
Next, create the body for this view by adding the title to a stack:
To make your life easier, you'll also modify the preview to show the different possible states of the text field all at once:
Note: You'll notice the text is .constant("some value"). The constant is a factory static function that creates a binding that never changes. It's useful for testing and previews, like in the above example.
Just like in the previous part of this SwiftUI course, you create multiple previews by adding views into a Group.
Now that you can see what you're doing, let's add the text field and the email icon.
Laying out SwiftUI views horizontally
With that out of the way, you can continue building the view by adding the text field and the icon. To make sure the text field and the icon are next to each other, you can use an HStack:
You're already familiar with VStack. Well, HStack works the same, except horizontally. You arrange a text field and an image in a horizontal stack.
The TextField is the SwiftUI equivalent of UIKit's UITextField, except it's plain by default — which is exactly what we need.
Note: If you want to style a text field to look like a regular UIKit text field, you can call textFieldStyle(RoundedBorderTextFieldStyle()) on the text field. The rounded border style is the one used by the good old UITextField.
In the previous part of this SwiftUI course, you learned about making image views expand to match their parent. In this case, you want the image to have a fixed size. That's why you call frame and pass it an exact width and height.
Displaying basic shape views in SwiftUI
Finally, you'll add the border on the bottom of the text field. Since the border is just a plain rectangular view with no content, you can use a Rectangle:
As its name suggests, Rectangle is just, well, a rectangle. Perfect for displaying borders or backgrounds. You set its height to 2 and leave other dimensions up to SwiftUI. You'll also set its color to red if showsError is true, otherwise, you'll use a light gray color.
Rectangle is only one of a few basic shape views. There's also RoundedRectangle, Circle, Capsule and Ellipse. Remember these when you need to draw shapes — there's no need to resort to CAShapeLayer anymore.
You now have a nice looking text field that is flexible enough to be used throughout your app. It's time to put it to action in the login screen!
Creating a SwiftUI login screen
Now, finally, you can get to work on the login screen! Head back to LoginView.swift and change body to the following:
First, you'll make sure stack view items are aligned to the left edge, just like in the last part of this SwiftUI course. Add a spacing of 26 points between each item and make sure the "Log In" text is styled as a title. You'll also add the system padding to the stack.
Remember that the text field you built receives a validation function? Let's create one that will validate an email. Add the following method to the struct:
This method checks the email string against a regular expression. If the email doesn't contain an @ sign, a dot, and text around both of those, this function will return false.
Also, add a state variable for the entered text:
With those two pieces in place, you can now add a text field for the email to your view:
Most of the properties should be self-explanatory, except maybe this weird $text thing. Don't worry, you're not programming PHP! $ is a special character that converts an @State variable to a binding. Remember, bindings are state variables that can be changed from a different view. In this case, LoginView will pass the email as a binding to ErrorTextField.
Next, create an empty function that you'll call when the user presses the login button:
Then, add a spacer and a button that calls this function. As you learned in the previous part of this SwiftUI course, the spacer will expand to make sure everything above it is on top, while the button is on the very bottom of the stack. Add the following to body:
Whenever the button is pressed, SwiftUI will call your login function which, currently, does absolutely nothing.
Don't be disappointed, you'll fix this soon.
Presenting a SwiftUI view asynchronously
Later in this course, login will perform a network request to log the user in and then present a contacts screen. For now, though, you'll navigate to an empty screen when the button is pressed.
Earlier, you learned how to use NavigationLink to present a new view in the navigation stack when a button is pressed. Often, though, you don't want to immidiately go to a new screen. You'll usually want to perform a check, a network request or some other bit of logic, and then present a new screen programmatically when you're done. SwiftUI has a way to do that, but it is a bit clumsy.
First, create a new state variable:
You'll navigate to the contacts screen when this variable gets set to true. To do this, you can use a NavigationLink, but differently from before. Instead of wrapping a button in the navigation link, you'll show a hidden link that the user won't see.
You'll also use a different NavigationLink initializer: NavigationLink(destination:isActive:label). The key here is isActive: This is a binding to a bool variable. When it gets set from false to true NavigationLink will know to trigger the presentation.
Add the navigation link to the bottom of body:
By making navigation link's body an EmptyView you make sure that the user can't see the link. You set its isActive binding to the state variable you created earlier.
Finally, set showContacts to true at the bottom of login:
When the user presses the login button, SwiftUI calls login, which sets showContacts to true, triggering the NavigationLink, which then presents an empty view in the navigation stack. This Rube Goldberg machine of events is what happens when you try to do an imperative thing, like presenting a view programmatically, in a declarative UI framework.
Presenting multiple SwiftUI views asynchronously
To expand this NavigationLink pattern to multiple views, you'd have to have one state variable for each view you want to present, leading to multiple flags in your view that can be false and true at the same time. This doesn't scale well.
To solve this problem, NavigationLink offers a third initializer: NavigationLink(_:destination:tag:selection:). While the one you used previously has a binding to a Boolean, this initializer is generic. selection is a binding to any Hashable type, like integers, strings and even enums. If the current value of selection matches the value of tag, the link will get triggered.
For instance, let's say you wanted to present either a login or a registration screen based on some logic. You'd start by defining an enum with cases for each of the views you'd like to present.
You'll also need a state variable to track which view should be shown:
Then, inside body, you can create hidden navigation links to each of your views.
When you want to present one of these two views, set viewToPresent to the corresponding value:
The navigation link will check the bound value, and if it matches its tag, present the view. This solution is easier to scale, so if you have more than one view you'd like to present, I suggest you use this approach.
And there you have it! You've built out a login screen and by doing so you've learned a bunch of important SwiftUI concepts:
How to present and style SwiftUI text fields, as well as how to use basic SwiftUI shape views.
How to use a NavigationView to show and style a navigation bar.
How to push a view when you press a button using NavigationLink.
How to push SwiftUI views programmatically using NavigationLink(destination:isActive) or NavigationLink(destination:tag:selection:).
I'd say that's a good day's work! No need to stop now, though.
In the next part, you'll learn all about SwiftUI lists by building out a contacts screen to show your user's friends. You'll also begin your journey into making network requests!