Directing Focus in SwiftUI

Moving the User’s attention using Focus 👀

Finally, we would like to address a new SwiftUI feature that was introduced for the first time in iOS 15 and is called “Focus” management. Focus is used to detect which control the user is currently interacting with. For example, we can detect that the user is editing a certain TextField. On the other hand, we can also control the Focus. For example, we can direct the user to a certain TextField and automatically open the keyboard to motivate the user to edit this particular TextField.

For example, after the user edited the “First Name” TextField in our OrderForm and taps the “Return” button on the keyboard, we can direct the Focus to the “Last Name” TextField and thus start the editing process of the “Last Name” TextField. This would look like this:

To achieve this we need to add a new property wrapper to our OrderForm: @FocusState. Since also no control can be focused at all, the FocusState can also be nil. Therefore it is necessary that the FocusState is an Optional.

@FocusState var focusedField: AddressField?

Finally, we need to declare the appropriate “AddressField” type. For this, we add the following enum to our Helper.swift file

enum AddressField: Int, Hashable {
    case firstName
    case lastName
    case street
    case city
    case zip
}

To connect the different TextFields to our FocusState, we use the .focused modifier for each.

TextField("First name", text: $firstName)
    .focused($focusedField, equals: .firstName)
TextField("Last name", text: $lastName)
    .focused($focusedField, equals: .lastName)
TextField("Street", text: $street)
    .focused($focusedField, equals: .street)
TextField("City", text: $city)
    .focused($focusedField, equals: .city)
TextField("ZIP code", text: $zip)
    .focused($focusedField, equals: .zip)

So as soon as a new TextField is edited, the FocusState changes to the corresponding case of our AddressField enum. 

If we now append the .onSubmit modifier to our “First Name” TextField, we can determine that the Focus is moved to the “Last Name” TextField. The code inside the .onSubmit modifier is triggered as soon as the user taps the return key on the keyboard.

TextField("First name", text: $firstName)
    .focused($focusedField, equals: .firstName)
    .onSubmit {
        focusedField = .lastName
     }

We repeat these steps for the remaining TextFields. However, after the user fills in the “ZIP” TextField and taps the return key, we want to dismiss the keyboard. We do this by assigning nil to our FocusState. 

TextField("First name", text: $firstName)
    .focused($focusedField, equals: .firstName)
    .onSubmit {
        focusedField = .lastName
     }
TextField("Last name", text: $lastName)
    .focused($focusedField, equals: .lastName)
    .onSubmit {
        focusedField = .street
    }
TextField("Street", text: $street)
    .focused($focusedField, equals: .street)
    .onSubmit {
        focusedField = .city
    }
TextField("City", text: $city)
    .focused($focusedField, equals: .city)
    .onSubmit {
        focusedField = .zip
    }
TextField("ZIP code", text: $zip)
    .focused($focusedField, equals: .zip)
    .onSubmit {
        focusedField = nil
    }

Let’s see if this works.

Great! You can see how we can control the editing Focus by linking our FocusState to the TextFields and moving the Focus accordingly.

Customizing our Keyboard using ToolbarItems ⌨️

Finally, we would like to enable the user to comfortably switch between the address TextFields while the keyboard is open. For this, we can add two Buttons to the keyboard to move up and down.

To do this, we simply append a .toolbar with a ToolBarItemGroup to the NavigationView.

NavigationView {
    //...
}
    .toolbar(content: {
        ToolbarItemGroup(placement: .keyboard, content: {

        })
    })
    //...
}

Now we can add two corresponding Buttons to this ToolBarItemGroup. However, we disable them when the user has reached the first or last TextField.

.toolbar(content: {
    ToolbarItemGroup(placement: .keyboard, content: {
        Button(action: {selectPreviousField()}) {
            Label("Previous", systemImage: "chevron.up")
        }
            .disabled(focusedField == .firstName ? true : false)
        Button(action: {selectNextField()}) {
            Label("Next", systemImage: "chevron.down")
               .disabled(focusedField == .zip ? true : false)
        }
    })
})

Now we only need the selectPreviousField and selectNextField function to change the FocusState and switch between the address fields accordingly.

    func selectNextField() {
        if focusedField == .firstName {
            focusedField = .lastName
        } else if focusedField == .lastName {
            focusedField = .street
        } else if focusedField == .street {
            focusedField = .city
        } else if focusedField == .city {
            focusedField = .zip
        } else {
            focusedField = nil
        }
    }

    func selectPreviousField() {
        if focusedField == .zip {
            focusedField = .city
        } else if focusedField == .city {
            focusedField = .street
        } else if focusedField == .street {
            focusedField = .lastName
        } else if focusedField == .lastName {
            focusedField = .firstName
        } else {
            focusedField = nil
        }
    }

Let’s run our app again to check if everything works:

Conclusion 🎊

We finished extending our Food Delivery App!

We learned how to work with Forms in SwiftUI. While doing this, we also how to use different control such as Sliders, Steppers, and Toggles. We also discovered system icons and used them for our app. 

We presented our new OrderForm view as a modal view that can be dismissed by swiping down or by using the cancel navigation bar item. You also learned how to control the user’s attention using Focus.

You already learned how to present new views by using a navigation view hierarchy or by overlaying them with modal views. But until now, we didn’t talk about navigating to new views independently. Let’s do this in the next chapter!

Leave a Reply

Your email address will not be published. Required fields are marked *