MacKuba

🍎 Kuba Suder's blog on Mac & iOS development

WWDC 21

Direct and reflect focus in SwiftUI

Categories: SwiftUI 0 comments Watch the video

SwiftUI in most cases manages focus automatically on your behalf based on given platform's conventions

In more complex cases, where SwiftUI can't figure this out by itself, there are APIs that let you customize the focus behavior

Example situations:

  • focusing the text editor of a new note when the "New" button is pressed in Notes
  • moving focus from a vertical list of buttons in the sidebar to a horizontal list of items in the main area on tvOS
  • programmatically dismissing the keyboard on iOS

The @FocusState API

A view property marked @FocusState is a special type of state that changes depending on which of the view's subviews is currently focused:

enum Field: Hashable {
    case email
    case password
}

struct ContentView: View {
    @FocusState private var focusedField: Field?

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
        }
    }
}

In this case, the Field type is a custom enum type, but you can also use strings, integers or any other Hashable type

Notice that the property is an optional, since it's set to nil if none of the fields is focused – in general, a @FocusState should always be optional

The binding is two-way – the value changes when the focus changes, but you can also move the focus by modifying the value

For example, you can move the focus back to the email field if the user tries to submit the form, but the email is invalid:

VStack {
    ...
}
.onSubmit {
    if !isEmailValid {
        focusedField = .email
    }
}

You can also dismiss the keyboard when the form is submitted by setting the value to nil:

VStack {
    ...
}
.onSubmit {
    if !isEmailValid {
        focusedField = .email
    } else {
        focusedField = nil
        logIn()
    }
}

Creating navigation targets (tvOS)

On tvOS, all navigation is done by moving focus and then selecting the focused element

You may sometimes want to be able to move focus between two parts of a screen with different content, and the default focus navigation will not work automatically if the two elements in two sections are not directly adjacent to each other

You can fix this by extending the effective focusable area of some elements like buttons to a larger area that is not normally focusable by itself

This is done using the new .focusSection API:

HStack {
    VStack {
        TextField("Email", text: $email)
        SecureField("Password", text: $password)
        SignInWithAppleButton(...)
    }
    .onSubmit { ... }
    .focusSection()

    VStack {
        Image(photoName)
        BrowsePhotosButton()
    }
    .focusSection()
}

When the .focusSection() view modifier is applied to a view like the VStack here, the view becomes capable of accepting focus as long as it contains any focusable subviews (the browse button)

Now, the user is able to move from the "Browse photos" button to the login sidebar on the left, even though the buttons in the sidebar aren't directly to the left of the button, and to move right from the email/password fields to the browse button, even though the button isn't directly to the right