Apple 为 iOS 15 引入了
@FocusState
和 @AccessibilityFocusState
及其各自的 API。通常,当我有一个支持多个版本的应用程序并且需要使用新的 API 时,我会用 if #available (iOS x) {}
包装代码或使用 @available
。
为了管理焦点状态,我需要使用 @AccessibilityFocusState
属性包装器声明一个 var,并且在 SwiftUI 视图中包含以下代码将导致它在 iOS 14 设备上运行时崩溃,尽管编译器没有任何抱怨:
@available(iOS 15.0, tvOS 15.0, *)
@AccessibilityFocusState var focus: FocusLocation?
在 tvOS 上,我可以使用编译器指令
#if os(tvOS) … #endif
进行有条件的编译,但这对于在运行时处理的 iOS 版本来说不是一个选项。
需要明确的是,我知道我无法在 iOS 14 设备上使用此 API,但放弃对 iOS 14 的支持完全是另一个问题
是否有办法为 iOS 15+ VoiceOver 用户使用此 iOS 15+ API,同时仍然允许普通 iOS 14 用户运行应用程序的其余部分?
事实证明有一个好方法来处理这个问题:将
@AccessibilityFocusState
放入自定义修饰符中。
@available(iOS 15, *)
struct FocusModifier: ViewModifier {
@AccessibilityFocusState var focusTarget: AccessibilityFocusTarget?
@Environment(\.lastAccessibilityFocus) @Binding var lastFocus
// this is the value passed into the modifier that associates an enum value with this particular view
var focusTargetValue: AccessibilityFocusTarget?
init(targetValue: AccessibilityFocusTarget) {
focusTargetValue = targetValue
}
func body(content: Content) -> some View {
content.accessibilityFocused($focusTarget, equals: focusTargetValue)
.onChange(of: focusTarget) { focus in
if focus == focusTargetValue {
lastFocus = focusTargetValue
}
}.onReceive(NotificationCenter.default.publisher(for: .accessibilityFocusAssign)) { notification in
if let userInfo = notification.userInfo,
let target = userInfo[UIAccessibility.assignAccessibilityFocusUserInfoKey] as? AccessibilityFocusTarget,
target == focusTargetValue
{
focusTarget = target
}
}
}
}
public extension View {
// Without @ViewBuilder, it will insist on inferring a single View type
@ViewBuilder
func a11yFocus(targetValue: AccessibilityFocusTarget) -> some View {
if #available(iOS 15, *) {
modifier(FocusModifier(targetValue: targetValue))
} else {
self
}
}
}
其中 AccessibilityFocusTarget 只是程序化焦点候选者的枚举:
public enum AccessibilityFocusTarget: String, Equatable {
case title
case shareButton
case favouriteButton
}
我将最后一个焦点元素存储为环境中的 AccessibilityFocusTarget 的绑定:
public extension EnvironmentValues {
private struct LastAccessibilityFocus: EnvironmentKey {
static let defaultValue: Binding<AccessibilityFocusTarget?> = .constant(nil)
}
var lastAccessibilityFocus: Binding<AccessibilityFocusTarget?> {
get { self[LastAccessibilityFocus.self] }
set { self[LastAccessibilityFocus.self] = newValue
}
}
}
.onReceive 块让我们能够通过 userInfo 中的 AccessibilityFocusTarget 值获取通知,并通过修饰符以编程方式将焦点设置到与该值关联的视图。
我添加了自定义通知和 userInfo 密钥字符串:
extension Notification.Name {
public static let accessibilityFocusAssign = Notification.Name("accessibilityFocusAssignNotification")
}
extension UIAccessibility {
public static let assignAccessibilityFocusUserInfoKey = "assignAccessibilityFocusUserInfoKey"
}
使用起来很简单。在 SwiftUI 视图层次结构的顶部,将某些内容注入到环境中的绑定中:
struct TopView: View {
@State var focus: AccessibilityFocusTarget?
var body: some View {
FirstPage()
.environment(\.lastAccessibilityFocus, $focus)
}
}
对于层次结构中可能成为编程焦点候选者的任何视图,只需使用修饰符将其与 AccessibilityFocusTarget 枚举值关联起来:
Title()
.a11yFocus(targetValue: .title)
ShareButton()
.a11yFocus(targetValue: .shareButton)
在任何这些子视图中都不需要其他任何东西 - 所有繁重的工作都在修改器中处理!
要设置焦点,只需使用NotificationCenter触发通知,焦点目标位于userInfo中:
public static func setAccessibilityFocus(_ target: AccessibilityFocusTarget, withDelayInSeconds delay: TimeInterval = 0) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
NotificationCenter.default.post(
name: .accessibilityFocusAssign,
object: nil,
userInfo: [UIAccessibility.assignAccessibilityFocusUserInfoKey: target as Any]
)
}
}
我正在使用包装视图来做到这一点:
@available(iOS 15, *)
public struct AccessibilityFocusableView<Content: View>: View {
@AccessibilityFocusState private var isFocused: Bool
private var requestFocusPublisher: AnyPublisher<Void, Never>
private var content: Content
public init(
requestFocusPublisher: AnyPublisher<Void, Never>,
@ViewBuilder content: () -> Content
) {
self.requestFocusPublisher = requestFocusPublisher
self.content = content()
}
public var body: some View {
self.content
.accessibilityFocused(self.$isFocused)
.onReceive(self.requestFocusPublisher) {
self.isFocused = true
}
}
}
像这样使用它:
if #available(iOS 15, *) {
AccessibilityFocusableView(shouldRequestFocus: myPublisher) {
myView
}
} else {
myView
}
我最终做了以下事情:
public extension View {
@ViewBuilder
func accessibilityFocused(isFocused: Binding<Bool>) -> some View {
if #available(iOS 15, *) {
self.modifier(EGAccessibilityVOFocusModifier(isFocused: isFocused))
} else {
self
}
}
}
private struct AccessibilityVOFocusModifier: ViewModifier {
@AccessibilityFocusState(for: .voiceOver) private var focused: Bool
@Binding private var isFocused: Bool
init(isFocused: Binding<Bool>) {
self._isFocused = isFocused
}
func body(content: Content) -> some View {
content
.accessibilityFocused($focused)
.onChange(of: isFocused) { newValue in
focused = newValue
}
}
}
这只是允许我在视图上创建一个状态变量并将其传递到这样的修改器中,并绕过在我的项目中设置的讨厌的最小 iOS:
struct ContentView: View {
@State private var isVOFocused = false
var body: some View {
Button(action: {
isVOFocused = true
}) {
// Button label
Text("Tap Me")
.font(.title)
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}.accessibilityFocused($isVOFocused)
}