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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Switching Focus Between Views

Open CheckoutFormView.swift. Inside body, add the following code after Form:

.onSubmit {
    if checkoutInFocus == .name {
      checkoutInFocus = .address
    } else if checkoutInFocus == .address {
      checkoutInFocus = .phone
    } else if checkoutInFocus == .phone {
      checkoutInFocus = nil
    }
}

This code defines what happens when the user taps return on the keyboard. If focus is on the Name field, it’ll shift to the Address field. If focus is on the Address field, it’ll shift to the Phone field. Finally, if focus is on the Phone field, it’ll release focus and dismiss the keyboard.

Build and run. Type any name inside the Name field and tap return. Check how focus shifts to the Address field. When you tap return again, focus shifts to the Phone field.

When the user presses the return key, the focus shifts between checkout fields

Note: If you’re unable to see the keyboard on Simulator when selecting a field, press Command-K or select I/OKeyboardToggle Software Keyboard from the Simulator menu.

Inside CheckoutFormView, find the validateAllFields function. Add the following code below TODO: Shift focus to the invalid field:

if !isNameValid {
  checkoutInFocus = .name
} else if !isAddressValid {
  checkoutInFocus = .address
} else if !isPhoneValid {
  checkoutInFocus = .phone
}

This code contains logic to shift focus to the first invalid field. validateAllFields is called when the user attempts to proceed to the next checkout step.

Build and run. On the Checkout screen, fill out the Name field, leave the Address field empty, and fill out the Phone field, then tap Proceed to Gift Message. Notice how focus shifts to the first invalid field, which is the Address field, in this case.

Focus shifts to the first invalid field in checkout form.

You made some great enhancements to focus management in the app so far… but now it’s time to make a mistake. In the next section, you’ll explore what happens when you aren’t careful with focus bindings.

Avoiding Ambiguous Focus Bindings

Everyone makes mistakes. In your case, it’ll be intentional! :] As the number of fields in a form grows, it can be easy to apply focus to the wrong field.

Inside recipientDataView, after the address EntryTextField, replace:

.focused($checkoutInFocus, equals: .address)

With:

.focused($checkoutInFocus, equals: .name)

This code binds the same checkoutInFocus key for both the Name and Address fields.

Build and run. Follow the steps below, and you’ll soon encounter issues with the Checkout Form:

  1. Go to the Checkout Form. Notice how the focus is on the Address field, not the Name field.
  2. Type in the Name field, then tap return. Notice how the keyboard is dismissed and focus doesn’t shift to the Address field.
  3. Finally, set the focus on the Phone field, then tap Proceed to Gift Message. Notice how focus doesn’t shift to the Address field despite it being invalid.

Wrong behavior in checkout focus.

All the strange behavior above is because you bound the same value to two views. Make sure to bind each key of your CheckoutFocusable enum only once to avoid ambiguous focus bindings.

Before you continue, undo the last change you made. Inside recipientDataView, after the address EntryTextField, set focused(_:equals:) back to:

.focused($checkoutInFocus, equals: .address)

In this section, you learned how to move focus between many views. Now, you’ll take your focus management skills further by handling focus in lists.

Managing Focus in Lists

On the Gift Message screen, the user can add multiple emails for the recipient. You’ll manage the focus inside that list of emails.

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

enum GiftMessageFocusable: Hashable {
  case message
  case row(id: UUID)
}

This code creates an enum to interact with FocusState inside GiftMessageView. The message key is to focus on the Gift Message field. The row key is to focus on the email list. You choose which item in the email list to focus on by using the id associated value.

Inside GiftMessageView, add the following property:

@FocusState private var giftMessageInFocus: GiftMessageFocusable?

This code creates a property to control focus between fields.

Next, in body, add the following after TextEditor:

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

If you’re thinking “auto-focus”, you’re right! Here’s what this code does:

  1. It binds the Gift Message field’s focus to the giftMessageInFocus property for the enum case message.
  2. When this view appears on screen, it shifts focus to the Gift Message field.

Build and run. Fill out the Checkout Form with valid entries and proceed to the Gift Message screen. Notice how the screen focuses on the Gift Message field when it appears.

Message screen starts with the focus on the message field

Similarly, find the recipientEmailsView definition. Inside ForEach, after EntryTextField, add the following line:

.focused($giftMessageInFocus, equals: .row(id: recipientEmail.id))

This code binds each email’s focus to giftMessageFocusable for the enum case row. To determine which email is in focus, you use the id of each email as the associated value.

Inside body, add the following code after Form:

.onSubmit {
  let emails = giftMessageViewModel.recipientEmails
  // 1
  guard let currentFocus = emails.first(where: { giftMessageInFocus == .row(id: $0.id) }) else { return }
  for index in emails.indices where currentFocus.id == emails[index].id {
    if index == emails.indices.count - 1 {
      // 2
      giftMessageInFocus = nil
    } else {
      // 3
      giftMessageInFocus = .row(id: emails[index + 1].id)
    }
  }
}

Here’s what this code does:

  1. It captures which Email field is now in focus, if there is one.
  2. When a user taps return while focusing on the last Email field, it releases focus and dismisses the keyboard.
  3. When a user taps return while focusing on any Email field besides the last one, it shifts focus to the Email field after it.

Find the validateFields function. Replace it with:

func validateFields() -> Bool {
  if !giftMessageViewModel.validateMessagePrompt.isEmpty {
    // 1
    giftMessageInFocus = .message
    return false
  } else {
    for (key, value) in giftMessageViewModel.validateEmailsPrompts where !value.isEmpty {
      // 2
      giftMessageInFocus = .row(id: key)
      return false
    }
    // 3
    giftMessageInFocus = nil
    return true
  }
}

Here’s what this code does:

  1. It shifts focus to the Gift Message field if it’s invalid.
  2. If any email from the email list is invalid, it shifts focus to that Email field.
  3. If all fields are valid, it releases focus and dismisses the keyboard.

Build and run. Go to the Gift Message screen, then tap Add new email twice. In the first Email field, type a valid email, then tap return to see how focus shifts to the second Email field. In the third Email field, type a valid email address, then tap Send the Gift to see how focus shifts to the invalid Email field.

Focus shifts between emails fields when the user presses return button.

You may notice that the code for both CheckoutFormView and GiftMessageView is a bit crowded. Moreover, both contain logic that should be in the ViewModel. In the next section, you’ll fix this and learn how to handle FocusState with MVVM.