xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • SwiftUI Accessibility Language:VoiceOver 多语言支持深度解析

SwiftUI Accessibility Language:VoiceOver 多语言支持深度解析

在构建多语言应用时,确保所有用户(包括依赖辅助技术的用户)都能获得一致的体验至关重要。SwiftUI 提供了强大的工具来增强应用的可访问性,但处理多语言场景下的 VoiceOver 发音问题可能需要一些技巧。本文将深入探讨如何精确控制 VoiceOver 的语言设置,提供实用的解决方案和代码示例,并分享最佳实践。

1. 理解问题:VoiceOver 的默认行为

VoiceOver 是 iOS 和 macOS 上的屏幕阅读器,它通过朗读屏幕内容来帮助视觉障碍用户与设备交互。默认情况下,VoiceOver 使用用户设备的区域设置(Locale)来决定如何朗读文本。这意味着如果用户的设备设置为英语,VoiceOver 会尝试用英语发音朗读所有文本,即使用户应用中的某些文本是其他语言。

考虑以下常见的多语言应用场景:一个语言学习应用同时显示英语和意大利语的问候语。

struct ContentView: View {
  private let english = "Good morning"
  private let italian = "Buongiorno"
  var body: some View {
    VStack(spacing: 8) {
      Text(english)
        .font(.headline)
      Text(italian)
        .font(.subheadline)
    }
    .frame(width:300, height: 200)
    .background(Color.yellow)
  }
}

对于设备设置为英语区域的用户,VoiceOver 会这样朗读:

  • "Good morning" - 发音正确(英语)
  • "Buongiorno" - 发音错误(尝试用英语发音读意大利语单词)

这种体验显然不理想,特别是对于语言学习应用来说。

2. SwiftUI 中的解决方案

2.1 使用 environment(\.locale) 修改器

在 SwiftUI 中,可以通过设置环境值来改变视图层次结构的区域设置。Locale 环境值会影响如何解释视图中的内容,包括 VoiceOver 的发音方式。

struct ContentView: View {
  private let english = "Good morning"
  private let italian = "Buongiorno"
  
  var body: some View {
    VStack(spacing: 8) {
      Text(verbatim: english)
        .font(.headline)
        .environment(\.locale, .init(identifier: "en"))
      
      Text(verbatim: italian)
        .font(.subheadline)
        .environment(\.locale, .init(identifier: "it"))
    }
    .frame(width:300, height: 200)
    .background(Color.yellow)
  }
}

关键点:

  • 使用 Text(verbatim:) 初始化器防止 SwiftUI 将字符串视为需要本地化的键
  • 为每种语言的文本设置相应的区域环境(英语为 "en",意大利语为 "it")
  • VoiceOver 现在会使用适当的发音:英语文本用英语发音,意大利语文本用意大利语发音

2.2 处理 Label 和 Button

对于更复杂的 UI 元素如 Label 和 Button,应用区域设置的方法略有不同。

Label 示例

Label {
  Text(verbatim: italian)
} icon: {
  Image(systemName: "globe")
}
.environment(\.locale, .init(identifier: "it"))

可以将区域设置应用于整个 Label 或其内部的 Text 视图。

Button 示例

// 使用 Text 视图作为标签的按钮
Button {
  // 按钮操作
} label: {
  Text(verbatim: italian)
    .environment(\.locale, .init(identifier: "it"))
}

// 使用 Label 的按钮(注意区域设置应用的位置)
Button {
  // 按钮操作
} label: {
  Label {
    Text(verbatim: italian)
  } icon: {
    Image(systemName: "globe")
  }
}
.environment(\.locale, .init(identifier: "it")) // 应用于按钮而不是标签

注意:当使用包含图标的 Label 时,区域设置需要应用于 Button 而不是内部的 Label 或 Text 视图,否则可能被忽略。这种情况下,VoiceOver 会以修改后的区域设置朗读"按钮"(例如 "Buongiorno, pulsante")。

2.3 使用 AttributedString(有限支持)

SwiftUI 支持使用 AttributedString,它有一个 .accessibilitySpeechLanguage 属性,理论上可以用于指定 VoiceOver 的语言:

private let italian = AttributedString(
  "Buongiorno",
  attributes: AttributeContainer([
    .accessibilitySpeechLanguage: "it"
  ])
)

然而,这种方法在实际应用中可能不如环境修改器可靠。

3. 理解 SwiftUI Text 初始化的细微差别

SwiftUI 中 Text 视图的初始化方式会影响其本地化行为,这对 VoiceOver 的语言处理有重要影响。

3.1 String 与 LocalizedStringKey

SwiftUI 的 Text 视图有两种主要的初始化方式:

let favoriteMovie = "The Lion King"
Text(favoriteMovie) // 使用 String 初始化器
Text("The Lion King") // 使用 LocalizedStringKey 初始化器

这两种方式看似相同,但行为有重要区别:

  • String 初始化器:将文本存储为标准 Swift 字符串,显示时不会尝试本地化
  • LocalizedStringKey 初始化器:假设字符串文字应该被本地化,会在主包中查找翻译

这种差异解释了为什么在使用 environment(\.locale) 时需要 Text(verbatim:) - 避免 SwiftUI 将字符串当作需要本地化的键。

3.2 Text 的解析时机

Text 视图在渲染时才会解析为最终显示的字符串。这个解析过程依赖于环境因素,包括:

  • 开发者通过视图修改器和环境值设置的显式配置
  • SwiftUI 基于文本放置位置的自动判断
  • 设备设置(深色模式、辅助功能文本大小、区域设置)

4. 更全面的辅助功能支持

除了语音语言设置,完整的 VoiceOver 支持还需要考虑其他辅助功能特性。

4.1 可访问性标签和提示

为 UI 元素提供清晰的标签和提示是基础的可访问性实践:

Image(systemName: "star.fill")
  .resizable()
  .frame(width: 100, height: 100)
  .accessibilityLabel("Favorite Star") // 描述元素是什么

Button("Tap me") {
  // 按钮操作
}
.accessibilityLabel("Tap me button") // 描述元素是什么
.accessibilityHint("Taps to perform an action") // 描述元素做什么

4.2 元素分组

相关的 UI 元素可以组合成单个可访问性元素,简化 VoiceOver 导航:

HStack {
  Image(systemName: "person.fill")
  Text("John Doe")
}
.accessibilityElement(children: .combine) // 组合子元素
.accessibilityLabel("Profile of John Doe") // 统一的描述

4.3 可访问性特征

可访问性特征帮助 VoiceOver 用户理解 UI 元素的用途:

Button("Submit") {
  // 提交操作
}
.accessibilityLabel("Submit button")
.accessibilityAddTraits(.isButton) // 明确标识为按钮

Text("Important Message")
  .font(.headline)
  .accessibilityAddTraits(.isHeader) // 标识为标题

4.4 动态类型支持

确保文本大小适应用户的偏好设置:

Text("Adjustable Text")
  .font(.body) // 尊重用户的文本大小设置
  .accessibilityLabel("Adjustable text")

5. 处理动态内容和自定义操作

5.1 可访问性操作

可以为元素添加自定义操作,增强 VoiceOver 用户的交互体验:

Button("Perform Action") {
  // 标准操作
}
.accessibilityLabel("Perform Action button")
.accessibilityAction {
  // VoiceOver 专用的自定义操作
}

5.2 动态内容通知

当应用内容动态变化时,通知 VoiceOver 用户:

struct DynamicListView: View {
  @State private var items = ["Item 1", "Item 2", "Item 3"]
  
  var body: some View {
    List(items, id: \.self) { item in
      Text(item)
        .accessibilityLabel(item)
    }
    .onAppear {
      // 模拟添加新项目
      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        items.append("New Item")
        // 通知布局变化
        UIAccessibility.post(notification: .layoutChanged, argument: nil)
      }
    }
  }
}

5.3 可访问性通知

对于重要事件,可以发送专门的通知:

struct NotificationExampleView: View {
  @State private var message = "Welcome to the app!"
  
  var body: some View {
    VStack {
      Text(message)
        .accessibilityLabel(message)
      
      Button("Change Message") {
        message = "You have a new notification!"
        // 让 VoiceOver 朗读新消息
        UIAccessibility.post(notification: .announcement, argument: message)
      }
    }
  }
}

6. 测试 VoiceOver 支持

充分测试是确保良好 VoiceOver 体验的关键。

6.1 启用 VoiceOver

在 iOS 设备或模拟器上启用 VoiceOver:

  1. 转到"设置"应用
  2. 选择"辅助功能"
  3. 选择"VoiceOver"并切换开启

在 macOS 上:

  1. 打开"系统偏好设置"
  2. 点击"辅助功能"
  3. 选择"VoiceOver"并勾选启用框

6.2 测试策略

测试时应考虑:

  • 导航流畅性:使用滑动手势在元素间导航,确保顺序合理
  • 描述准确性:确认标签和提示准确描述每个元素
  • 上下文完整性:分组元素应提供足够的上下文信息
  • 操作可用性:所有功能应可通过 VoiceOver 操作访问
  • 动态更新:内容变化时应适当通知用户

6.3 真实用户测试

尽可能让依赖辅助技术的真实用户测试应用,他们的反馈往往能发现开发者忽略的问题。

7. 高级主题和边缘情况

7.1 可访问性树理解

SwiftUI 会为所有可访问性元素创建可访问性树(Accessibility Tree),这个树状数据结构帮助辅助功能系统在不同元素间导航。它只提取视图 body 中具有可访问性元素的视图对象。

理解可访问性树有助于:

  • 优化 VoiceOver 导航体验
  • 识别不必要的可访问性元素
  • 确保重要内容不被遗漏

7.2 条件可访问性标签

有时需要根据元素状态动态更新可访问性标签:

struct FavoriteButton: View {
  @State private var isSuperFavorite = false
  
  var body: some View {
    Button(action: { isSuperFavorite.toggle() }) {
      Image(systemName: isSuperFavorite ? "sparkles" : "star.fill")
    }
    .accessibilityLabel("Super Favorite", isEnabled: isSuperFavorite)
  }
}

iOS 18 引入了带 isEnabled 参数的可访问性修饰符,仅在条件为真时生效。

7.3 拖放可访问性

实现可访问的拖放交互:

struct CommentAlertView: View {
  var body: some View {
    CommentAlertView(contact: contact)
      .onDrop(of: [.audio], delegate: delegate)
      .accessibilityDropPoint(.leading, description: "Set Sound 1")
      .accessibilityDropPoint(.center, description: "Set Sound 2")
      .accessibilityDropPoint(.trailing, description: "Set Sound 3")
  }
}

7.4 小组件可访问性

为交互式小组件添加可访问性支持:

struct BeachWidgetView: View {
  var body: some View {
    ForEach(beaches) { beach in
      BeachView(beach)
        .accessibilityAction(named: "Favorite", 
                            intent: ToggleRatingIntent(beach: beach, rating: .fullStar))
        .accessibilityAction(.magicTap, 
                            intent: ComposeIntent(type: .photo))
    }
  }
}

8. 最佳实践总结

  1. 始终提供有意义的可访问性标签:每个交互元素都应具有描述其用途的标签。
  2. 使用适当的环境设置:对于多语言内容,使用 environment(\.locale) 和 Text(verbatim:) 确保正确发音。
  3. 分组相关元素:使用 accessibilityElement(children: .combine) 将逻辑相关的元素组合在一起。
  4. 支持动态类型:确保文本大小适应用户的偏好设置。
  5. 测试真实场景:在实际设备上使用 VoiceOver 全面测试应用。
  6. 处理动态内容更新:当内容变化时通知 VoiceOver 用户。
  7. 考虑边缘情况:如拖放交互、小组件和条件界面。
  8. 持续改进:可访问性是一个持续过程,随着应用发展不断改进。

9. 完整示例

以下是一个整合了上述技术的完整示例:

struct MultilingualContentView: View {
  private let greetings = [
    ("Hello", "en"),
    ("Bonjour", "fr"),
    ("Hola", "es"),
    ("こんにちは", "ja")
  ]
  
  var body: some View {
    VStack(spacing: 20) {
      Text("Multilingual Greetings")
        .font(.title)
        .accessibilityAddTraits(.isHeader)
      
      ForEach(greetings, id: \.0) { greeting, languageCode in
        HStack {
          Image(systemName: "globe")
            .accessibilityHidden(true) // 隐藏装饰性图标
            
          Text(verbatim: greeting)
            .font(.title2)
            .environment(\.locale, .init(identifier: languageCode))
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
        .accessibilityElement(children: .combine)
        .accessibilityLabel("Greeting in \(languageCode): \(greeting)")
      }
      
      Button("Play All Greetings") {
        // 播放所有问候语的音频
      }
      .accessibilityLabel("Play all greetings")
      .accessibilityHint("Plays greetings in all supported languages")
    }
    .padding()
  }
}

总结

在 SwiftUI 中管理 VoiceOver 的语言设置需要理解环境修改器、文本初始化和可访问性API的协同工作。通过使用 environment(\.locale) 修改器结合 Text(verbatim:) 初始化器,开发者可以确保多语言内容被正确朗读。此外,全面的 VoiceOver 支持还需要考虑可访问性标签、分组、特征和动态内容更新。

最后更新: 2025/9/10 15:21