DEV Community

Cover image for Creating a login screen in SwiftUI
marinbenc🐧 for CometChat

Posted on • Edited on • Originally published at cometchat.com

Creating a login screen in SwiftUI

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.

Creating a login screen in SwiftUI

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":

struct LoginView: View {

  var body: some View {
    Text("Log In")
  }

}
Enter fullscreen mode Exit fullscreen mode

You'll fill up this placeholder screen later, but you'll first need a way to navigate to the screen.

I am creating this course in collaboration with CometChat, a modern chat platform to help you add chat to you Swift app. During the next few weeks, we’ll be releasing installments of our free SwiftUI course here on dev.to! In the course, you’ll dive deep into SwiftUI by building a real-world production-scale chat app, learning SwiftUI in a practical way, on a scale larger than a simple example app. Follow me to get notified of future parts of this course! You can also follow @CometChat on Twitter see the course of CometChat’s blog.

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:

let contentView = NavigationView { WelcomeView() }
Enter fullscreen mode Exit fullscreen mode

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.

SwiftUI fixing Navigation default padding

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:

var body: some View {
  VStack(alignment: .leading) {
    ...
  }
  .navigationBarTitle("Create an account")
}
Enter fullscreen mode Exit fullscreen mode

If you run the project, you might notice there's now two texts saying "Create an account".

Adding a navigation bar title in SwiftUI

Remove the one inside the VStack:

var body: some View {
  VStack(alignment: .leading) {

    /* Remove from here...
    Text("Create an account")
      .modifier(BodyText())
      .padding()
    ... to here*/

    Text("Connect with people around the world")
      .modifier(TitleText())
      .padding([.bottom, .leading, .trailing])

    ...
}
Enter fullscreen mode Exit fullscreen mode

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):

var body: some View {
  VStack(alignment: .leading) {

    ...

    VStack(spacing: 30) {
      // Change this line:
      NavigationLink(destination: LoginView()) {
        PrimaryButton(title: "Log In")
      }

      Button(action: { }) {
        SecondaryButton(title: "Sign Up")
      }
    }

    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Presenting a new view from a button in SwiftUI

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:

Making a text field in SwiftUI

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:

struct ErrorTextField: View {

  let title: String
  let placeholder: String
  let iconName: String
  let text: Binding<String>
  let keyboardType: UIKeyboardType
  let isValid: (String) -> Bool

  init(title: String,
    placeholder: String,
    iconName: String,
    text: Binding<String>,
    keyboardType: UIKeyboardType = UIKeyboardType.default,
    isValid: @escaping (String)-> Bool = { _ in true}) {

    self.title = title
    self.placeholder = placeholder
    self.iconName = iconName
    self.text = text
    self.keyboardType = keyboardType
    self.isValid = isValid
  }

}
Enter fullscreen mode Exit fullscreen mode

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:

var showsError: Bool {
  if text.wrappedValue.isEmpty {
    return false
  } else {
    return !isValid(text.wrappedValue)
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

var body: some View {
  VStack(alignment: .leading) {
    Text(title)
      .foregroundColor(Color(.lightGray))
      .fontWeight(.bold)
  }
}
Enter fullscreen mode Exit fullscreen mode

To make your life easier, you'll also modify the preview to show the different possible states of the text field all at once:

struct ErrorTextField_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      ErrorTextField(
        title: "Email",
        placeholder: "test@email.com",
        iconName: "email",
        text: .constant(""))
        .padding()
        .previewLayout(.fixed(width: 400, height: 100))

      ErrorTextField(
        title: "Email",
        placeholder: "test@email.com",
        iconName: "email",
        text: .constant("some@email.com"))
        .padding()
        .previewLayout(.fixed(width: 400, height: 100))

      ErrorTextField(
        title: "Email",
        placeholder: "test@email.com",
        iconName: "email",
        text: .constant("someemail.com"),
        isValid: { _ in false })
        .padding()
        .previewLayout(.fixed(width: 400, height: 100))
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Previewing multiple states of a SwiftUI view at once.

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:

var body: some View {
  VStack(alignment: .leading) {

    Text(title)
      .foregroundColor(Color(.lightGray))
      .fontWeight(.bold)

    // New code:
    HStack {
      TextField(placeholder, text: text)
        .keyboardType(keyboardType)
        .autocapitalization(.none)
      Image(iconName)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 18, height: 18)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Creating a horizontal stack with HStack in SwiftUI

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:

var body: some View {
  VStack(alignment: .leading) {
    Text(title)
      .foregroundColor(Color(.lightGray))
      .fontWeight(.bold)

    HStack { ... }

    // New code:
    Rectangle()
      .frame(height: 2)
      .foregroundColor(showsError ? 
        .red : 
        Color(red: 189 / 255, green: 204 / 255, blue: 215 / 255))
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Using SwiftUI Rectangle to create a border for a view

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:

var body: some View {
  VStack(alignment: .leading, spacing: 26) {
    Text("Log In")
      .modifier(TitleText())
  }
  .padding()
}
Enter fullscreen mode Exit fullscreen mode

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.

Creating a login screen in SwiftUI

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:

private func isValid(email: String) -> Bool {
  let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
  let predicate = NSPredicate(format:"SELF MATCHES %@", regex)
  return predicate.evaluate(with: email)
}
Enter fullscreen mode Exit fullscreen mode

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:

@State private var email = ""
Enter fullscreen mode Exit fullscreen mode

With those two pieces in place, you can now add a text field for the email to your view:

var body: some View {
  VStack(alignment: .leading, spacing: 26) {
    Text("Log In")
      .modifier(TitleText())

    // New code:
    ErrorTextField(
      title: "Email",
      placeholder: "mail@example.com",
      iconName: "email",
      text: $email,
      keyboardType: .emailAddress,
      isValid: isValid)
  }
  .padding()
}
Enter fullscreen mode Exit fullscreen mode

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.

Creating a login screen in SwiftUI

Next, create an empty function that you'll call when the user presses the login button:

private func login() {
  // Initiate network request
}
Enter fullscreen mode Exit fullscreen mode

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:

var body: some View {
  VStack(alignment: .leading, spacing: 26) {
    ...

    Spacer()

    Button(action: login) {
      PrimaryButton(title: "Log In")
    }
  }
  .padding()
}
Enter fullscreen mode Exit fullscreen mode

Whenever the button is pressed, SwiftUI will call your login function which, currently, does absolutely nothing.

Creating a login screen in SwiftUI

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:

@State private var showContacts = false
Enter fullscreen mode Exit fullscreen mode

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:

var body: some View {
  VStack(alignment: .leading, spacing: 26) {
    ...

    Button(action: login) {
      PrimaryButton(title: "Log In")
    }

    NavigationLink(destination: EmptyView(), isActive: $showContacts) {
      EmptyView()
    }
  }
  .padding()
}
Enter fullscreen mode Exit fullscreen mode

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:

showContacts = true
Enter fullscreen mode Exit fullscreen mode

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.

enum PresentedView {
  case login
  case registration
}
Enter fullscreen mode Exit fullscreen mode

You'll also need a state variable to track which view should be shown:

@State private var viewToPresent: PresentedView?
Enter fullscreen mode Exit fullscreen mode

Then, inside body, you can create hidden navigation links to each of your views.

NavigationLink(
  destination: LoginView(), 
  tag: .login, 
  selection: $viewToPresent) {
  EmptyView()
}

NavigationLink(
  destination: RegistrationView(), 
  tag: .registration, 
  selection: $viewToPresent) {
  EmptyView()
}
Enter fullscreen mode Exit fullscreen mode

When you want to present one of these two views, set viewToPresent to the corresponding value:

viewToPresent = .login
Enter fullscreen mode Exit fullscreen mode

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.

Conclusion

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!

Top comments (2)

Collapse
 
mklagassey profile image
mklagassey

Writing this for others who may run into the same situation. I was getting a consistent error: "Type 'ErrorTextField' does not conform to protocol 'View'" on the ErrorTextField struct so I tried reloading xcode and even downloaded the completed project files from github to compare the swift file. They were the same but my code caused the error which prevented a successful build. Very frustrating.

The fix turned out to be simply copy/pasting the entire code from the completed project file over my own. When I reverted back to the previous code which had caused the error (which still wasn't complete as I didn't get any further than adding the preview code) it stopped throwing the error even though nothing had changed.

Lesson learned, don't trust the error messages in SwiftUI yet. Many are not that helpful and some of them are misleading or completely wrong!

Collapse
 
jmconde profile image
Juan Manuel

Hi, there is a mistake
Where it say
let contentView = NavigationView { WelcomeView() }
Must it say
let contentView: some View = NavigationView {
WelcomeView()
}
I found this in the code
I am learning swift, I love this course!!
Sorry for my english.
Juan Manuel