Irbisa · Cheatsheet

Accessibility & Localization

VoiceOver, traits, Dynamic Type, Reduce Motion, String Catalogs, plurals, RTL, locale formatting

Middle Developer · 25 questions

  1. 01

    What does making an iOS app accessible involve: which users do you build for, and which system features do you support?

    It means the app still works for someone who cannot see the screen, needs large text, cannot tell colours apart, feels sick from motion or cannot touch precisely.

  2. 02

    What are an element's accessibility label, value, traits and hint, and in what order does VoiceOver read them?

    VoiceOver reads label, value, traits, hint: the label says what the element is, the value its current state, the traits what kind of element it is and how it behaves, and the hint what happens when you activate it.

    // wrong: type and state are baked into the labelButton(action: toggle) { Image(systemName: icon) }  .accessibilityLabel(isOn ? "Favourite button, on" : "Favourite button, off") // right: each piece in its own propertyButton(action: toggle) { Image(systemName: icon) }…
  3. 03

    What does VoiceOver say as it swipes through this row, and what would you change?

    struct UserRow: View {  let user: User  let onDelete: () -> Void   var body: some View {    HStack {      Image("avatar_placeholder")      Text(user.name)      Spacer()      Button(action: onDelete) {        Image("ic_trash")      }    }  }}

    Three stops, two of them noise: the placeholder's asset name as an image, the user's name, and the trash icon's asset name as a button.

  4. 04

    What is Dynamic Type, and how do you support it in SwiftUI and UIKit?

    Dynamic Type is the system-wide text size setting; text set in a text style such as .body or .headline scales with it automatically, across seven standard sizes and five larger accessibility sizes.

    // SwiftUIText("Order history").font(.headline)            // scalesText(order.note)  .font(.custom("Brand", size: 15, relativeTo: .subheadline))Text("v2.4.1").font(.system(size: 11))           // fixed: never scales…
  5. 05

    How do you localize an iOS app, and what must never be hardcoded?

    Add the languages to the project, keep every user-facing string in a String Catalog (Localizable.xcstrings) that Xcode fills from your code on each build, and let the user's locale format numbers, dates and prices.

    // SwiftUI: literals are localization keysText("Order history")Button("Delete", role: .destructive) { delete() } // Outside a view: resolve now, and give the translator contextlet title = String(localized: "Delete order?",…
  6. 06

    A product card is five VoiceOver stops: image, title, price, badge, heart button. How do you make it one, and when should you not?

    Wrap the card in .accessibilityElement(children: .combine) so it becomes one element whose label joins the children's, and expose the heart as a named custom action; in UIKit set isAccessibilityElement = true on the container and write its label.

    HStack {  AsyncImage(url: product.imageURL)    .accessibilityHidden(true)             // decoration: hide before combining  VStack(alignment: .leading) {    Text(product.name)    Text(product.price.formatted(.currency(code: product.currency)))…
  7. 07

    VoiceOver reads a card's price before its title, although the title is drawn above it. How do you control the reading order?

    In SwiftUI group the views with .accessibilityElement(children: .contain) and order them with .accessibilitySortPriority (higher is read first); in UIKit set the container's accessibilityElements array in the order you want.

    // SwiftUI: price is declared first but drawn below the title; the title must be read firstZStack(alignment: .topLeading) {  Text(album.price)    .offset(y: 24)    .accessibilitySortPriority(1)  Text(album.title)…
  8. 08

    A row has swipe-to-delete and a custom volume dial. How does a VoiceOver user reach them without those gestures?

    Every gesture needs an accessibility equivalent: custom actions (.accessibilityAction(named:), accessibilityCustomActions) appear on VoiceOver's Actions rotor, and a value control becomes adjustable so swiping up and down changes it.

    MessageRow(message: message)  .accessibilityAction(named: "Delete") { delete(message) }  .accessibilityAction(named: "Mark as read") { markRead(message) } VolumeDial(value: $volume)  .accessibilityLabel("Volume")…
  9. 09

    After a save VoiceOver jumps to the top of the screen and re-reads it. Which notification was posted by mistake and which should be used?

    It posted .screenChanged, which tells VoiceOver a new screen appeared and moves focus to its first element; a status message should be an .announcement, which is spoken without moving focus.

    // wrong: resets the user's position and re-reads the screenUIAccessibility.post(notification: .screenChanged, argument: "Saved") // right: speak, and leave focus aloneUIAccessibility.post(notification: .announcement,                     argument: String(localized: "Draft saved"))…
  10. 10

    A form fails validation. How do you move VoiceOver focus to the error message in SwiftUI?

    Attach .accessibilityFocused(_:) to the error view, bind it to an @AccessibilityFocusState property, and set that property once the error is on screen; VoiceOver moves its cursor there and reads it.

    struct SignUpForm: View {  @State private var email = ""  @State private var error: String?  @AccessibilityFocusState private var errorFocused: Bool   var body: some View {…
  11. 11

    QA reports that this row ignores the user's text size, and that once it scales it clips at the largest sizes. What do you change?

    HStack(spacing: 12) {  Image(systemName: "envelope")    .frame(width: 40, height: 40)  Text(item.title)    .font(.system(size: 17))    .lineLimit(1)}.frame(height: 44)

    Use a text style so the text scales, then remove every fixed dimension around it: minHeight instead of height, a @ScaledMetric icon size, no single-line limit, and a vertical layout at accessibility sizes.

  12. 12

    Find the bug: the user raises the text size from Control Center, and this header's labels grow but stay side by side and clip until the screen is reopened.

    final class ContactHeaderViewController: UIViewController {  private let stack = UIStackView(arrangedSubviews: [UILabel(), UILabel()])  override func viewDidLoad() {    super.viewDidLoad()    for case let label as UILabel in stack.arrangedSubviews {      label.font = .preferredFont(forTextStyle: .body)      label.adjustsFontForContentSizeCategory = true    }    view.addSubview(stack)    stack.frame = view.bounds  }  override func viewWillAppear(_ animated: Bool) {    super.viewWillAppear(animated)    stack.axis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory      ? .vertical : .horizontal  }}

    The axis is chosen only in viewWillAppear, which does not run when the text size changes while the screen is open: the labels follow Dynamic Type live, but the stack stays horizontal, so the larger text clips.

  13. 13

    What are Reduce Motion, Reduce Transparency, Increase Contrast and Bold Text, and how do you honour them in your own UI?

    They are system accessibility settings that UIKit and SwiftUI honour in their own controls; for your own animations, blurs, colours and fonts you read them, as SwiftUI environment values or UIAccessibility flags, and adapt.

    struct Hero: View {  let isOpen: Bool  let parallaxOffset: CGFloat  @Environment(\.accessibilityReduceMotion) private var reduceMotion  @Environment(\.accessibilityReduceTransparency) private var reduceTransparency  @Environment(\.colorSchemeContrast) private var contrast…
  14. 14

    Design marks paid orders with a green dot and overdue ones with a red dot. What is wrong with that, and how do you fix it?

    Colour is the only carrier of the meaning, so the status is lost for colour-blind users and completely for VoiceOver; add a second signal (a shape or a word) and put the status into the accessibility label or value.

    struct OrderRow: View {  let order: Order   var body: some View {    HStack {      Image(systemName: order.isPaid ? "checkmark.circle.fill"…
  15. 15

    How do you test an app's accessibility: what do the Accessibility Inspector and performAccessibilityAudit() catch, and what do they miss?

    Automated audits catch the mechanical problems on one screen (missing labels, small hit areas, low contrast, clipped text), while only a person using VoiceOver can tell whether the labels make sense and the task can be finished.

    final class AccessibilityAuditTests: XCTestCase {  func testHomeScreenPassesAudit() throws {    let app = XCUIApplication()    app.launch()     try app.performAccessibilityAudit(for: [.contrast, .hitRegion, .textClipped]) { issue in…
  16. 16

    What is a String Catalog, and what do the New, Needs Review and Stale states mean after a build?

    A String Catalog (.xcstrings) is a JSON file Xcode keeps in sync with your code: every build extracts localizable literals into it and tracks each translation's state, and it is compiled into ordinary .strings and .stringsdict files in the app bundle.

  17. 17

    The string is "\(count) items" and the Russian build shows «1 товаров». How should plurals be handled?

    With a plural variation in the String Catalog: the system picks a CLDR plural category for the number and each language supplies its own forms, so your code never decides between singular and plural.

    // wrong: an English-only rule baked into the call sitelet label = count == 1 ? "1 item" : "\(count) items" // right: one key, every language brings its own formsText("\(count) items")              // catalog key: "%lld items"…
  18. 18

    In the German build the navigation title is translated but the section titles stay English: why, and which type fixes it?

    struct LibrarySection: Identifiable {  let id = UUID()  let title: String} let sections = [LibrarySection(title: "Recent"),                LibrarySection(title: "Favourites")] List(sections) { section in  Text(section.title)}.navigationTitle("Library")

    Text localizes only literals: Text(section.title) with a String uses the verbatim initializer, and "Recent" stored as a String was never extracted.

  19. 19

    How do you format dates, numbers and prices for the user, and who decides the format when the language is English but the region is Germany?

    Use the .formatted() APIs and let Locale.current decide.

    // wrong: the developer chose the pattern and the symbollet df = DateFormatter()df.dateFormat = "MM/dd/yyyy hh:mm a"Text("$\(order.total)") // right: the locale decides presentation, the data decides the currency…
  20. 20

    Find the bug: in the French and German builds of the country picker, several countries sit at the very bottom of the list.

    struct CountryPicker: View {  let regionCodes: [String]            // "AE", "AT", "DE", "ZW", …  @Binding var selection: String   private var countries: [(code: String, name: String)] {    regionCodes      .map { ($0, Locale.current.localizedString(forRegionCode: $0) ?? $0) }      .sorted { $0.name < $1.name }  }   var body: some View {    Picker("Country", selection: $selection) {      ForEach(countries, id: \.code) { Text(verbatim: $0.name).tag($0.code) }    }  }}

    < on String compares Unicode code points, not the user's alphabet, so names starting with an accented letter (Égypte, Österreich) sort after Z, and lowercase after uppercase.

  21. 21

    Product wants a language switcher inside the app: what does iOS already give you, and why is writing AppleLanguages to UserDefaults a hack?

    iOS has had a per-app language setting since iOS 13: if the app ships more than one localization, its page in Settings shows a Language option, and an in-app "switcher" should simply open that page with UIApplication.openSettingsURLString.

  22. 22

    How do you support right-to-left languages like Arabic: what mirrors automatically, what do you flip yourself, and what must never flip?

    With leading and trailing layout, UIKit and SwiftUI mirror the interface automatically; you decide per image whether it is directional, keep things like media controls and logos unmirrored, and let the Unicode bidirectional algorithm order the text.

    // A directional custom icon: mirror it in RTLImage("arrow_next")  .flipsForRightToLeftLayoutDirection(true) // Media controls keep their direction in a mirrored screenPlayerControls()…
  23. 23

    A bar chart is drawn in a single Canvas or a draw(_:) override. How do you make each bar reachable with VoiceOver?

    A drawn chart is one opaque element, so you build its accessibility tree by hand: .accessibilityChildren in SwiftUI, or UIAccessibilityElements with frames in UIKit, plus a summary, and ideally a chart descriptor for Audio Graphs.

    struct SalesChart: View {  let sales: [MonthlySales]          // Identifiable: month, amount  var body: some View {    Canvas { context, size in drawBars(sales, in: context, size: size) }      .accessibilityLabel("Sales by month")      .accessibilityChildren {        // never drawn, only exposed to VoiceOver…
  24. 24

    Translations arrive two weeks before release. How do you catch truncation and hardcoded strings in CI long before that?

    Run UI tests under Xcode's pseudolanguages and the largest text size: they stretch, accent and bracket every localized string, so clipping, glued sentences and never-extracted literals show up with no translator involved.

    final class LocalizationLayoutTests: XCTestCase {  func testCheckoutFitsLongStringsAndLargeText() throws {    let app = XCUIApplication()    // Language and region come from the test plan configuration    app.launchArguments += [      "-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL",…
  25. 25

    Beyond the app UI, where do you localize the App Store listing, push notifications and App Shortcuts?

    Each lives outside your view code: the store listing in App Store Connect, push text as loc-key resolved on the device or in a Notification Service Extension, and App Shortcut phrases in their own AppShortcuts.xcstrings.