Apple introduced the @FocusState and @AccessibilityFocusState and their respective APIs for iOS 15. Typically when I have an app that supports multiple versions and I need to use a new API, I would wrap the code with if #available (iOS x) {} or use @available.
For managing focus state, I need to declare a var with the @AccessibilityFocusState property wrapper, and literally including the following code in a SwiftUI View will cause it to crash at runtime on an iOS 14 device, although the compiler has no complaints:
@available(iOS 15.0, tvOS 15.0, *)
@AccessibilityFocusState var focus: FocusLocation?
On tvOS, I can use the compiler directive #if os(tvOS) … #endif to this compile conditionally, but this isn't an option for iOS versions which are handled at runtime.
To be clear, I know that I can’t use this API for iOS 14 devices, but dropping support for iOS 14 is another issue entirely
Is there anyway to use this iOS 15+ API for iOS 15+ VoiceOver users, and still allow general iOS 14 users to run the rest of the app?
It turns out there is a good way to handle this: put
@AccessibilityFocusStatein a custom modifier.where AccessibilityFocusTarget is just an enum of programmatic focus candidates:
And I'm storing the last focused element as a Binding to AccessibilityFocusTarget in the environment:
The .onReceive block lets us will take a notification with the AccessibilityFocusTarget value in userInfo and programmatically set focus to the View associated with that value via the modifier.
I've added a custom notification and userInfo key string:
Using this is simple. At the top of your SwiftUI View hierarchy, inject something into the binding in the environment:
And for any Views within the hierarchy that might be candidates for programmatic focus, just use the modifier to associate it with a AccessibilityFocusTarget enum value:
Nothing else is need in any of those child views - all the heavy lifting is handled in the modifier!
To set focus, just fire off a notification using NotificationCenter, with the focus target in userInfo: