Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 051 / 100

Accessibility in SwiftUI: a hands-on guide for engineers

Apps everyone can use.

Expona Admin6 min read

Most engineering teams build features for themselves first, then retrofit accessibility later—if at all. By that point, you're rewriting core UI logic and fighting against architectural decisions. Accessibility in SwiftUI: a hands-on guide for engineers shows you how to embed accessible practices into your build process from day one, with minimal overhead and maximum reach. We'll walk through concrete patterns that let visually impaired users, those with motor limitations, and people in high-motion environments all navigate your app smoothly.

Why accessibility matters to your business

Accessibility isn't charity—it's engineering. In the UK alone, 11 million people live with sight loss; globally, over a billion people experience some form of disability. If your app is on the App Store, you're potentially excluding a market segment worth billions in aggregate spending power. iOS users also tend to enable accessibility features at higher rates than other platforms because the OS has had strong VoiceOver and dynamic text support for over a decade.

From a technical perspective, accessible code is usually more maintainable. When you label your UI elements properly and respect system settings for motion and contrast, you're forced to separate concerns, test more thoroughly, and write less fragile layout logic. Teams that prioritize accessibility often report fewer crashes and simpler view hierarchies.

Labels: accessibilityLabel for icons and images

VoiceOver users hear nothing when they tap an unlabeled icon. Your app becomes a black box. The fix is straightforward: every image, button, and interactive element without visible text needs an accessibilityLabel.

Start with images that convey meaning:

swift
Image(systemName: "heart.fill")
    .foregroundColor(.red)
    .accessibilityLabel("Add to favorites")

Notice we didn't label it "heart.fill"—that's the system name, not the user-facing meaning. VoiceOver users need to know what the button does, not what icon it displays.

For interactive elements without text, use both accessibilityLabel and accessibilityHint:

swift
Button(action: { presentSettings() }) {
    Image(systemName: "gear")
}
.accessibilityLabel("Settings")
.accessibilityHint("Opens the app settings screen")

The label is what VoiceOver announces immediately; the hint is optional context that appears after a pause, like a tooltip for screen reader users.

For decorative images—say, a background pattern or an icon purely for visual rhythm—hide them from accessibility entirely:

swift
Image("decorative-border")
    .accessibilityHidden(true)

This prevents VoiceOver from announcing every decorative element and cluttering the experience. A common mistake is labeling decorative content, which slows down navigation for blind users.

Complex images like charts or diagrams need more thought. A single label won't do: "Chart showing Q4 revenue" tells you nothing useful. Consider adding a accessibilityValue with a summary, or link to a detailed text description elsewhere in the UI:

swift
Image("revenue-chart")
    .accessibilityLabel("Q4 Revenue by Region")
    .accessibilityValue("North: $2.1M, South: $1.8M, West: $2.4M, East: $1.9M")

Dynamic Type: use text styles so fonts scale

On iOS, users can enable Larger Accessibility Sizes to make text bigger—sometimes dramatically. If your layout is hardcoded to 16-point Helvetica, you'll break older users' workflows and frustrate them into uninstalling.

Always use semantic text styles:

swift
VStack(spacing: 12) {
    Text("Welcome")
        .font(.largeTitle)
    
    Text("Your daily summary")
        .font(.title2)
    
    Text("You've completed 3 of 5 tasks today.")
        .font(.body)
    
    Text("Tap to view details")
        .font(.caption)
}

These styles scale proportionally when users enable Large or Extra-Large text sizes. The system handles the math; you just declare the hierarchy.

Test your layouts at multiple sizes. Xcode's preview supports this:

swift
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .environment(\.sizeCategory, .small)
                .previewDisplayName("Small")
            
            ContentView()
                .environment(\.sizeCategory, .extraExtraExtraLarge)
                .previewDisplayName("Extra Large")
        }
    }
}

Run this preview and watch your layout adapt. If text overflows or buttons become unreachable, adjust your spacing and constraints. Remember: a user with vision loss who relies on larger text is not an edge case; they're part of your core audience.

Avoid fixed frame heights for text:

swift
// Bad: text gets cut off at larger sizes
Text("Hello").frame(height: 20)

// Good: text size determines height
Text("Hello").lineLimit(1)

VoiceOver: test flows with VoiceOver on

Reading your accessibility labels in Xcode is not the same as navigating your app blindfolded. Spend an hour actually using VoiceOver. It's humbling and reveals problems no code review will catch.

Enable VoiceOver on your device: Settings → Accessibility → VoiceOver → On. Or in the simulator: Cmd+U then search for VoiceOver.

Now navigate your app. Swipe right to move forward, swipe left to move backward. Double-tap to activate. Flick up then down (two-finger) to read everything on screen. You'll discover:

  • Which buttons are unreachable because their labels are cut off mid-word.
  • Which container grouping is confusing (five separate images announced individually instead of one cohesive card).
  • Which interactive elements have no labels at all and are silent voids.
  • Which tab order is backwards because SwiftUI's default navigation order doesn't match your visual layout.

Fix tab order with accessibilityElement(children:):

swift
VStack {
    Button("Save") { save() }
    Button("Cancel") { cancel() }
}
.accessibilityElement(children: .combine)

This tells VoiceOver to treat the whole VStack as one element, reducing verbosity. Alternatively, set explicit navigation:

swift
Button("Save") { save() }
    .accessibilityElement()

Button("Cancel") { cancel() }
    .accessibilityElement()

If you have a list or carousel, group items logically so VoiceOver doesn't announce every thumbnail separately:

swift
VStack(spacing: 16) {
    ForEach(products) { product in
        HStack {
            Image(product.imageName)
            VStack(alignment: .leading) {
                Text(product.name).font(.headline)
                Text(product.price).font(.body)
            }
        }
        .accessibilityElement(children: .combine)
        .accessibilityLabel(product.name)
        .accessibilityValue(product.price)
    }
}

Now each product is one navigable item with a name and price, not five separate elements.

Contrast and motion: respect Reduce Motion and Increase Contrast

Not all accessibility is about vision or hearing. Some users have vestibular disorders that trigger nausea from motion; others have low vision and need higher contrast text.

Reduce Motion: If your app has animated transitions, parallax effects, or bouncing icons, check the system setting before enabling them:

swift
@Environment(\.accessibilityReduceMotion) var reduceMotion

var body: some View {
    VStack {
        Image("logo")
            .scaleEffect(reduceMotion ? 1.0 : 1.2)
            .animation(reduceMotion ? nil : .spring())
    }
}

When reduceMotion is true, the animation disappears entirely. The image still appears; it just doesn't bounce. This is kind and costs nothing.

For transitions between screens, check before using .transition():

swift
if isPresented {
    SettingsView()
        .transition(reduceMotion ? .identity : .slide)
}

Increase Contrast: The system also offers a setting to bump up contrast for users with low vision. Respect it with lighter text on dark backgrounds and darker text on light ones. SwiftUI's built-in colors like .primary and .secondary adapt automatically:

swift
Text("Important info")
    .foregroundColor(.primary)  // Respects Increase Contrast
    .font(.body)

Avoid custom colors that don't meet WCAG AA standards (4.5:1 contrast for normal text). Use a contrast checker tool; it takes two minutes and prevents accessibility failures later.

Common mistakes

  • Labeling everything: Don't add a label to every Text view. VoiceOver reads visible text automatically. Label only images, icons, and hidden elements.
  • Ignoring system settings: Building an app that ignores reduceMotion or largeContentSizeCategory is like ignoring the user's choice of language. Test at multiple size categories and with motion disabled.
  • Complex labels: "Tap the three-line menu button in the top-left to navigate" is too verbose. "Menu" or "Navigation" is enough. Be concise.
  • No tab order testing: Assuming default navigation is correct without testing. Always verify with VoiceOver on.
  • Decorative elements stealing focus: Dividers, spacers, and background images shouldn't be announced. Use accessibilityHidden(true) liberally.

Key takeaways

  • Every icon and image without text needs an accessibilityLabel describing its purpose, not its appearance.
  • Use semantic text styles (.body, .largeTitle, etc.) so your layouts scale with user-selected text sizes.
  • Test with VoiceOver enabled on real devices or simulator for at least an hour per release cycle.
  • Disable animations and motion effects when accessibilityReduceMotion is enabled to prevent vestibular discomfort.
  • Group related UI elements with accessibilityElement(children: .combine) to reduce navigation noise.
  • Use system colors like .primary and .secondary to respect contrast settings automatically.
  • Accessibility code is maintenance code; it makes your app more robust and readable for all engineers on the team.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles