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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Focus Management in SwiftUI: Getting Started
25 mins
- Getting Started
- Applying Auto-Focus
- Improving Focus Implementation for Multiple Views
- Switching Focus Between Views
- Avoiding Ambiguous Focus Bindings
- Managing Focus in Lists
- Improving Focus Implementation With MVVM
- Observing Values From Focused Views
- Modifying Values From Focused Views
- Where to Go From Here?
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.
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.
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:
- Go to the Checkout Form. Notice how the focus is on the Address field, not the Name field.
- Type in the Name field, then tap return. Notice how the keyboard is dismissed and focus doesn’t shift to the Address field.
- 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.
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:
- It binds the Gift Message field’s focus to the
giftMessageInFocus
property for the enum casemessage
. - 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.
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:
- It captures which Email field is now in focus, if there is one.
- When a user taps return while focusing on the last Email field, it releases focus and dismisses the keyboard.
- 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:
- It shifts focus to the Gift Message field if it’s invalid.
- If any email from the email list is invalid, it shifts focus to that Email field.
- 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.
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.