DEV Community

Swipewalk dev
Swipewalk dev

Posted on AI-assisted

Seven native Android and iOS accessibility quirks that are easy to miss until you run the app

Some accessibility problems in native apps look fine in code review and only show up on a device: with TalkBack or VoiceOver on, the text size turned up, or the theme switched. Here are seven I ran into while testing small native sample apps (Android Views and Jetpack Compose, iOS UIKit and SwiftUI) on emulators, Simulators and real phones, with what to do about each.

1. SwiftUI: .accessibilityLabel() replaces the visible text

Button("Pay") { pay() }
    .accessibilityLabel("Submit")
Enter fullscreen mode Exit fullscreen mode

On screen the button says "Pay". Its accessibility label is "Submit", and when we dumped every attribute XCUITest exposes for that button, "Pay" appeared nowhere. Someone using voice control who says "Tap Pay" may not reach it, and a screen reader user hears a different word from the one sighted colleagues refer to. This is the case WCAG 2.5.3 Label in Name (Level A) was written for: the accessible name should contain the visible text.

In our UIKit sample the visible title stayed in the tree next to the label; in SwiftUI only the label did (iOS 26.5 Simulator). Keep the visible word in the label, and put extra context in a hint:

Button("Pay") { pay() }
    .accessibilityHint("Pays the parking ticket")
Enter fullscreen mode Exit fullscreen mode

2. SF Symbols can come with a name, but maybe not the right one

An image button using UIImage(systemName: "magnifyingglass") with no label set came out with the accessible name "Search" in our tests (an iOS 26.5 Simulator and an iPhone on iOS 27). We saw this for that one symbol only. It's a generic name for the symbol, not for what your button does. If the button opens a filter or searches something specific, say so:

searchButton.accessibilityLabel = "Search tickets"
Enter fullscreen mode Exit fullscreen mode

On Android, an ImageButton or a Compose Icon button without a description gets no default name from the platform:

IconButton(onClick = onSearch) {
    Icon(Icons.Default.Search, contentDescription = "Search tickets")
}
Enter fullscreen mode Exit fullscreen mode

3. UIKit text doesn't follow the text size setting unless you ask it to

SwiftUI text styles scale with Dynamic Type automatically. UIKit labels need two things, a text-style font and adjustsFontForContentSizeCategory, or they don't update when the text size setting changes. On our two small sample screens, Apple's accessibility audit flagged 3 elements with unsupported Dynamic Type in the UIKit version and 1 in the SwiftUI version. One of the UIKit ones was a text field whose font had simply never been set.

label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true

// Custom font that still scales:
label.font = UIFontMetrics(forTextStyle: .body)
    .scaledFont(for: UIFont(name: "Avenir-Medium", size: 17)!)
label.adjustsFontForContentSizeCategory = true
Enter fullscreen mode Exit fullscreen mode

Both versions need adjustsFontForContentSizeCategory = true.

In SwiftUI, .custom(_:size:) already scales with the body style; add relativeTo: to scale with a different style. Only .custom(_:fixedSize:) stays fixed, so watch for that one.

Text("Payment history").font(.custom("Avenir-Medium", size: 17, relativeTo: .headline))
Enter fullscreen mode Exit fullscreen mode

4. Compose: a button made of several texts is read as all of them

A clickable Compose element merges its children for TalkBack and reads them in order. In our sample, a button built from two Texts ("Pay" and "$25.00") was announced as "Pay, $, 25.00, Button": TalkBack read the amount in two parts. And when one child had a description that didn't match its visible text ("Pay" described as "Submit", followed by "now"), TalkBack said "Submit, now, Button" while the screen showed "Pay now". (Android 16 emulator with TalkBack 16.0.0, and a Pixel 4a on Android 13 with TalkBack 17.0.1.)

If the parts don't read well together, give the whole button one name that contains the visible words:

Button(
    onClick = onPay,
    modifier = Modifier.semantics { contentDescription = "Pay $25.00" }
) {
    Row(Modifier.clearAndSetSemantics { }) {
        Text("Pay")
        Text("$25.00")
    }
}
Enter fullscreen mode Exit fullscreen mode

clearAndSetSemantics clears everything after it in the modifier chain, so put it on the content, not on the Button's own modifier, or the button loses its click action.

5. Hard-coded backgrounds with system-styled controls

In our UIKit sample the screen's background was hard-coded white, while a standard system-styled button (UIButton.Configuration.gray()) kept following the appearance setting. In dark appearance its title measured about 1.48:1 against the light gray behind it, well under the 4.5:1 WCAG 1.4.3 (Contrast (Minimum), AA) asks for normal-size text. In light appearance there was no issue (iOS Simulator). The likely cause: system-styled parts change with the appearance, the hard-coded background doesn't.

Don't mix a hard-coded background with system-styled controls. Use semantic colours throughout:

view.backgroundColor = .systemBackground
label.textColor = .label
Enter fullscreen mode Exit fullscreen mode

The same mismatch is possible on Android when android:windowBackground is fixed but text colours come from a DayNight theme. Check every screen in both light and dark mode.

6. Android: the same screen got a pane title on Android 13 but not on Android 16

TalkBack can announce a pane title when content changes. The same Views screen in our sample had a pane title on an Android 13 Pixel and none on the Android 16 emulator; we didn't find out why. (A pane title is separate from the window or activity title, which TalkBack can also announce.) So don't rely on it happening by itself. Give each activity a meaningful title, and set a pane title explicitly for fragments or panes that change within one window:

ViewCompat.setAccessibilityPaneTitle(paneView, "Payment history")
Enter fullscreen mode Exit fullscreen mode
// Compose
Column(Modifier.semantics { paneTitle = "Payment history" }) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Then turn TalkBack on and listen when you move between screens.

7. android:labelFor works for TalkBack, but not every tool sees it

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Ticket number"
    android:labelFor="@id/ticket" />
<EditText
    android:id="@+id/ticket"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
Enter fullscreen mode Exit fullscreen mode

TalkBack reads this field as "Edit box for Ticket number" (Android 16 emulator, TalkBack 16.0.0). But some testing tools build on uiautomator dump, whose output has no attribute for that link, so they may report the field as unlabelled. If a scanner flags a field you labelled with labelFor, check it with TalkBack before changing anything. A TextInputLayout hint is another way to name the field:

<com.google.android.material.textfield.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Ticket number">
    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
Enter fullscreen mode Exit fullscreen mode

How to check your own app

  • Turn on TalkBack or VoiceOver and swipe through each screen. Does every control have a name that matches what's on screen? Do screens announce a title?
  • Turn the text size up to the largest setting while the app is open. Does the text grow? Is anything cut off or out of reach? (Before Android 14 the Settings maximum is usually below 200%; adb shell settings put system font_scale 2.0 tests 200%, and 1.0 sets it back. From Android 14, large text grows non-linearly, so big headings grow less than body text by design.)
  • Switch between light and dark mode and look at contrast again.
  • Try voice control (Voice Access or Voice Control): can you activate buttons by saying what you see?

Automated checks catch only some accessibility problems. Testing with the screen readers yourself, and with people who use them, is still what counts.

I found these while testing small native sample apps for accessibility. If you've hit other platform quirks like these, I'd like to hear about them in the comments.

This article was created with the help of AI. The findings come from tests I ran with AI tooling on Android emulators, iOS Simulators and real phones (a Pixel 4a and an iPhone SE), on small sample apps.

Top comments (2)

Collapse
 
humam_moin profile image
Humam Moin •

Great examples! It's interesting how accessibility can behave differently on Android and iOS. Some of these issues are easy to miss during development, so testing with TalkBack and VoiceOver on real devices is really important.

Collapse
 
swipewalk-dev profile image
Swipewalk dev •

Thanks! The platform differences surprised me most, like the same intent ending up with different names: SwiftUI drops the visible text from the tree, while UIKit keeps it. And agreed on real devices. Some things only showed up on the phone, like controls pushed off the bottom of a small screen at large text. Have you run into any platform-specific ones yourself?