Focus Management in SwiftUI: Getting Started

Learn how to manage focus in SwiftUI by improving the user experience for a checkout form. By Mina H. Gerges.

5 (3) · 1 Review

Download materials
Save for later
Share

Remember the last time you logged in, completed a checkout process, or sent feedback? Each of these interactions likely included a form. Navigating a form can be tedious if the app doesn’t assist with focus. When a view is focused, it’s visually activated and ready for interaction. A view type you might associate with focus is a text field: Often, focus is applied to text fields to bring up the keyboard and tip off the user to type in that field next.

To simplify focus implementation, Apple introduced FocusState at WWDC 2021. FocusState is a property wrapper that tracks and edits the focus location in the scene.

In this tutorial, you’ll learn all about focus management in SwiftUI by using modifiers and wrappers like FocusState to help users navigate forms more effectively. You’ll do so by filling out a checkout form and gift card for a friend. How nice of you! :]

Swifty with shopping bags.

While finding the perfect gift, you’ll learn how to:

  • Switch focus between views.
  • Handle focus in a list while using the MVVM pattern.
  • Recognize and edit a focused view from another view.

Getting Started

Download the starter project by clicking Download Materials at the top or bottom of this tutorial. Open the project in the starter directory in Xcode. Build and run on an iPhone simulator.

The app displays a list of available gifts. Select a gift, then enter shipping information. Finally, write a gift message along with the recipient’s email addresses.

The user selects a gift and then proceeds to the checkout process.

You may notice that some focus improvements could be made. For example, focus should shift seamlessly between shipping fields when the user taps the return key. And, when trying to proceed to the next step, focus should draw the user to invalid entries.

In the next section, you’ll learn about FocusState and how it can help the user start filling out your form quickly.

If you’re looking for model-view-viewmodel (MVVM) pattern specifically, checkout Design Patterns by Tutorials: MVVM.

Note: The app supports the model-view-viewmodel (MVVM) pattern. To learn more about various design patterns, check out Design Patterns by Tutorials.

If you’re looking for model-view-viewmodel (MVVM) pattern specifically, checkout Design Patterns by Tutorials: MVVM.

Applying Auto-Focus

You’ll start improving the app by implementing auto-focus. Auto-focus is the effect where the first relevant view automatically receives the focus upon loading the screen. Though subtle, it’s an experience users expect.

You’ll use the FocusState property wrapper to achieve this effect. Generally, FocusState covers many things:

  • Keeping track of which view is currently focused.
  • Changing focus to a desired view.
  • Removing focus from all views, resulting in keyboard dismissal.

Two modifiers complement FocusState:

  • focused(_:): A modifier that binds the view’s focus to a single Boolean state value.
  • focused(_:equals:): A modifier that binds the view’s focus with any given state value.

FocusState and focused(_:) are perfect to fix the first UX bug in your app.

Open CheckoutFormView.swift. Below the struct declaration, add the following line:

@FocusState private var nameInFocus: Bool

This code creates a property to control focus for the name field.

Inside recipientDataView, add the following modifier to the first EntryTextField:

.focused($nameInFocus)

This code binds focus for the name field to the nameInFocus property. nameInFocus‘s value changes to true each time the user sets focus on this field.

Add the following under the code you just added:

.onAppear {
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
    self.nameInFocus = true
  }
}

This code programmatically applies focus to the Name field when the checkout view appears on screen. The 0.75 second delay is to ensure the view has already appeared.

Build and run. Select a gift, then tap the Checkout button. Once checkout appears, notice how focus shifts to the Name field.

Shows how the focus shifts to the name textfield in checkout screen.

Auto-focus achieved! Doesn’t that make filling out the Checkout Form a bit easier?

In a form with multiple fields, you’ll invariably want to support focus for most fields to assist the user in navigating quickly by tapping return on the keyboard.

With your current implementation, you’d have to add another Bool property for every field requiring focus. In the next section, you’ll use a better technique to avoid cluttering your view.

Improving Focus Implementation for Multiple Views

Open CheckoutFormView.swift. At the top, add the following code, right before CheckoutFormView:

enum CheckoutFocusable: Hashable {
  case name
  case address
  case phone
}

This code creates an enum listing all the focusable fields.

Next, inside CheckoutFormView, replace:

@FocusState private var nameInFocus: Bool

With:

@FocusState private var checkoutInFocus: CheckoutFocusable?

This code creates a property with the type of the enum you just created. This single property holds which field is in focus instead of requiring three different Boolean properties.

Inside recipientDataView, after the first EntryTextField, replace the following code:

// 1
.focused($nameInFocus)
.onAppear {
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
    // 2
    self.nameInFocus = true
  }
}

With:

// 1
.focused($checkoutInFocus, equals: .name)
.onAppear {
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
    // 2
    self.checkoutInFocus = .name
  }
}

Here’s what this code does:

  1. It uses the focused(_:equals:) modifier to bind the Name field’s focus to the checkoutInFocus property for the enum case name.
  2. When this view appears on screen, it shifts focus to the Name field by setting the value of checkoutInFocus to the corresponding enum case.

The CheckoutFocusable enum was declared as Hashable because that’s a requirement for the type used with focused(_:equals:).

Build and run. You’ll find the app does exactly as before: When the Checkout screen loads, the Name field is auto-focused.

A checkout form with focus on name field.

Now, for those other fields. Inside recipientDataView, add the following modifier to the second EntryTextField:

.focused($checkoutInFocus, equals: .address)

This code binds the Address field’s focus to the checkoutInFocus property for the enum case address.

You might’ve guessed what you’ll do with the Phone field! Inside recipientDataView, add the following modifier to the final EntryTextField:

.focused($checkoutInFocus, equals: .phone)

This code binds the Phone field’s focus to the checkoutInFocus property for the enum case phone.

At this point, you’ve set up all the form fields on the checkout screen to handle focus. Next, you’ll use this setup to switch focus between the fields and improve the validation experience.