SwiftUI Accessibility Language

How do you change the accessibility language used by VoiceOver with SwiftUI?

What’s The Problem?

If you’re working on any sort of language learning application you might want a view showing text in two different languages:

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)
  }
}

yellow card showing “Good morning” above “Buongiorno”

This causes a problem for VoiceOver when it speaks the two lines of text. By default, VoiceOver speaks with the user’s locale. If my device is using an English locale it pronounces “Good morning” fine but sounds ridiculous when attempting “Buongiorno”. I want VoiceOver to use an English pronunciation for the former and an Italian one for the latter.

Accessibility Language

If I was using UIKit I’d set the accessibilityLanguage property on the text labels to override the language VoiceOver uses when speaking:

englishLabel.accessibilityLanguage = "en"
italianLabel.accessibilityLanguage = "it"

Unfortunately, there’s no such view modifier in SwiftUI. So how do I change the language VoiceOver uses when working with a SwiftUI view?

One suggestion I’ve seen is to use AttributedString which does have an .accessibilitySpeechLanguage attribute:

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

That doesn’t work for me.

Changing the Locale

A tip on Mastodon from Ɓukasz Rutkowski suggested changing the locale environment for each view:

Text(english)
  .font(.headline)
  .environment(\.locale, .init(identifier: "en"))
Text(italian)
  .font(.subheadline)
  .environment(\.locale, .init(identifier: "it"))

That didn’t work until I realised the Text view was treating the text as a localized string key. I never want to localize those values. Using the verbatim initializer for the Text view fixed the problem:

Text(verbatim: english)
  .font(.headline)
  .environment(\.locale, .init(identifier: "en"))
Text(verbatim: italian)
  .font(.subheadline)
  .environment(\.locale, .init(identifier: "it"))

VoiceOver now pronounces the English text in English and the Italian text in Italian.

Label and Button

The need to use a verbatim String does force you to use the longer form for views like a Label:

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

You can apply the locale to the label or the child text view. A Button works the same way if I use a Text view for the label:

Button {
} label: {
  Text(verbatim: italian)
    .environment(\.locale, .init(identifier: "it"))
}

However, if I use a Label to include an icon the locale only takes effect when applied to the Button. It’s ignored when applied to the child Label or Text view:

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

That does mean VoiceOver pronounces “button” in the modified locale (in the above case I hear “Buongiorno, pulsante”) but I can live with that.