The @AccessibilityFocusState property wrapper (SwiftUI Release 3, iOS 15+) reads and moves the
VoiceOver or Switch Control cursor, mirroring how @FocusState handles the keyboard. Use it to
send the assistive cursor to an error, an alert, or the first field of a form.
Boolean Bindings
Declare an @AccessibilityFocusState boolean and bind it with
.accessibilityFocused($flag). SwiftUI keeps the value in sync with the assistive cursor, and
writing true moves the cursor to that view.
struct SignInView: View {
@AccessibilityFocusState
private var isEmailFocused: Bool
@State private var email = ""
var body: some View {
Form {
TextField("email", text: $email)
.accessibilityFocused($isEmailFocused)
}
.onChange(of: isEmailFocused) { newValue in
print(newValue)
}
}
}
By default the value aggregates every assistive technology on the device. Scope it with
@AccessibilityFocusState(for: .switchControl) or .voiceOver when only one should drive it.
One Enum for Many Elements
Like @FocusState, the wrapper accepts any Hashable value. Model the focusable elements as an
enum, bind each view with .accessibilityFocused($focus, equals: .case), and keep the property
optional so the framework can set nil when focus leaves your views.
enum FocusableField: Hashable {
case email
case password
}
struct ContentView: View {
@AccessibilityFocusState
private var focus: FocusableField?
@State private var email = ""
@State private var password = ""
var body: some View {
Form {
TextField("email", text: $email)
.accessibilityFocused($focus, equals: .email)
SecureField("password", text: $password)
.accessibilityFocused($focus, equals: .password)
Button("login", action: login)
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
focus = .email
}
}
}
}
The article delays the initial focus = .email slightly after onAppear, so the hierarchy
exists before the cursor moves. Assigning the enum value at any point moves VoiceOver or Switch
Control to that element.