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 3 of 4 of this article. Click here to view the first page.

Improving Focus Implementation With MVVM

Open CheckoutViewModel.swift, and add the following property:

@Published var checkoutInFocus: CheckoutFocusable?

This code creates a property to control the focus of checkout from CheckoutViewModel.

Add the following lines below TODO: Toggle Focus:

func toggleFocus() {
  if checkoutInFocus == .name {
    checkoutInFocus = .address
  } else if checkoutInFocus == .address {
    checkoutInFocus = .phone
  } else if checkoutInFocus == .phone {
    checkoutInFocus = nil
  }
}

This code handles what happens when the user taps the return key on the Checkout screen. If it looks familiar, that’s because it’s extracted from onSubmit(of:_:) in CheckoutFormView.

Next, add the following lines below TODO: Validate all fields:

func validateAllFields() {
    let isNameValid = validateNamePrompt.isEmpty
    let isAddressValid = validateAddressPrompt.isEmpty
    let isPhoneValid = validatePhonePrompt.isEmpty

    allFieldsValid = isNameValid && isAddressValid && isPhoneValid

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

Again, this is extracted from CheckoutFormView into the ViewModel. Now, you’ll update CheckoutFormView to use these functions.

Open CheckoutFormView.swift. Inside body, replace onSubmit(of:_:) with the following:

.onSubmit {
  checkoutViewModel.toggleFocus()
}

Instead of keeping the logic inline, you use the toggleFocus function you just created in the ViewModel.

Next, find giftMessageButton. Inside CustomButton, replace:

validateAllFields()

With:

checkoutViewModel.validateAllFields()

Again, inline logic is replaced with logic now in the ViewModel. To clean up, remove the validateAllFields function from CheckoutFormView.

Build and run. Go to the Checkout screen. In the Name field, type a name and tap return. Did you encounter strange behavior? The focus doesn’t shift to the Address field. Also, if you tap Proceed to Gift Message, focus doesn’t shift to the first invalid field.

Wrong behavior inside checkout page after applying MVVM

You’ve simply copy-pasted which object your logic lives in. So, what causes this wrong behavior?

The reason is that you have two checkoutInFocus properties. One is inside CheckoutFormView, while the other is inside CheckoutViewModel. But, neither of them is aware of the other’s changes. When validating fields, for example, you only update checkoutInFocus inside CheckoutViewModel. You’ll fix this now.

Open CheckoutFormView.swift. Inside recipientDataView, after Section, add the following lines:

.onChange(of: checkoutInFocus) { checkoutViewModel.checkoutInFocus = $0 }
.onChange(of: checkoutViewModel.checkoutInFocus) { checkoutInFocus = $0 }

This code syncs the changes that happen to checkoutInFocus, so both know the current state.

Build and run. Go to the Checkout screen. In the Name field, type a name and tap return. Notice that behavior is back to working as expected.

Shifting focus in Checkout page after applying MVVM

It’s your turn! Extract the logic for managing focus state from GiftMessageView to GiftMessageViewModel. You have all the necessary knowledge, but if you need some help, feel free to refer to the final project.

Now that you’ve improved the app’s user experience for iPhone users, it’s time to attend to iPad users. Your app has an additional feature for larger layouts that would benefit from some focus.

Observing Values From Focused Views

Build and run on an iPad simulator. Go to the Gift Message screen. On the right, you’ll notice a new view available on the iPad layout. It displays a preview of the gift card.

While the user types a gift message, your code observes that text. You’ll show the user a live update of the card’s appearance with the message in it. To do so, you’ll use FocusedValue, another property wrapper introduced to manage focus state.

Gift Message screen in iPad with card preview on the right.

Create a new file inside the ViewModel folder. Name it FocusedMessage.swift. Add these lines inside this new file:

import SwiftUI
// 1
struct FocusedMessage: FocusedValueKey {
  typealias Value = String
}

// 2
extension FocusedValues {
  var messageValue: FocusedMessage.Value? {
    get { self[FocusedMessage.self] }
    set { self[FocusedMessage.self] = newValue }
  }
}

Here’s what this code does:

  1. It creates a struct conforming to the FocusedValueKey protocol. You need to add the typealias for Value to fulfill this protocol. The type of Value is the type of content to observe. Because you want to observe the gift message, the correct type is String.
  2. It creates a variable to hold the FocusedValue called messageValue with a getter and setter.

The FocusedValueKey protocol and the extension of FocusedValues is how you can extend the focused values that SwiftUI propagates through the view hierarchy. If you’ve ever added values to the SwiftUI Environment, you’ll recognize it’s a very similar dance.

Next, you’ll use the messageValue variable to observe changes in the user’s gift message.

Open GiftMessagePreview.swift, and add the following property:

@FocusedValue(\.messageValue) var messageValue

This code creates a property to observe the newly created messageValue.

Inside body, after GeometryReader, add the following lines:

Text(messageValue ?? "There is no message")
  .padding()

This code uses messageValue to show a live update of the user’s message over the background image.

Finally, open GiftMessageView.swift. Find TextEditor inside body, and add the following modifier to it:

.focusedValue(\.messageValue, giftMessageViewModel.checkoutData.giftMessage)

This code binds the changes that happen to the giftMessage property in the ViewModel to messageValue. But, what changes giftMessage?

Notice the initialization for the text field: TextEditor(text: $giftMessageViewModel.checkoutData.giftMessage). The binding passed into TextEditor triggers updates to the giftMessage property as the user types in the text field. In turn, messageValue is updated because it’s now bound to giftMessage. Lastly, messageValue is observed and displayed on a different view. The result is that any text typed in the Gift Message field will reflect in the preview.

Build and run. Go to the Gift Message screen. Notice how the preview on the right shows the same text as the message field on the left. Change the text inside the Gift Message field, and notice how a live update occurs in the preview on the right even though they’re two different views.

Preview text on the right reflects changes of the text on the left.

Just like that, you’re now reading the value of a focusable view in one view from another. In the next section, you’ll take it one step further by modifying a focused value between views.

Modifying Values From Focused Views

You’ll add a little personality to the gift card by replacing plain text with emojis! :]

Open FocusedMessage.swift. Replace the FocusedMessage struct with the following lines:

struct FocusedMessage: FocusedValueKey {
  typealias Value = Binding<String>
}

In this code, you change the type of Value from String to Binding<String> to enable updating its value in addition to observing it.

Open GiftMessagePreview.swift. Replace FocusedValue with:

@FocusedBinding(\.messageValue) var messageValue

Again, you change the type to be a binding — in this case FocusedBinding — to enable modification.

Inside body, find ZStack. Add the following modifier to Text, after padding(_:_:):

.onChange(of: messageValue) { _ in
  giftMessageViewModel.checkTextToEmoji()
}

This code tracks the changes in the message, then checks if the last word can be converted to an emoji.

Next, inside emojiSuggestionView, add the following code within the first parameter of Button:

if let message = messageValue {
  messageValue = TextToEmojiTranslator.replaceLastWord(from: message)
}

This code modifies messageValue directly. It replaces the last word with the matched emoji if the user taps the emoji button.

Finally, open GiftMessageView.swift. Inside body, replace focusedValue(_:_:) of TextEditor with:

.focusedValue(\.messageValue, $giftMessageViewModel.checkoutData.giftMessage)

The addition of that little $ keeps the compiler happy now that the type for messageValue is Binding.

Build and run. Go to the Gift Message screen. In the message field, type :D. Notice how the Suggested Emoji on the right shows 😃. Tap this emoji, and watch how the message text on the left changes to show the emoji.

Change the text in gift message to show matched emoji

Now, both views are able to effect change on one another.

You’ve come a long way in improving the experience of your app by being thoughtful with focus management. Time to get yourself a gift!

Swifty celebrating.