跳到正文
DEV Community· Swipewalk dev·· 3 小時前AI 評分52

原生 Android 與 iOS App 執行後容易發現的七項無障礙問題

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

AI 導讀

作者測試原生 Android 與 iOS 示例 App,整理出七項在程式碼審查時容易忽略的無障礙問題。測試涵蓋 Android Views、Jetpack Compose、iOS UIKit 和 SwiftUI,問題包括無障礙名稱與畫面文字不一致、文字未隨系統設定放大,以及明暗模式下對比不足。

正文

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

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

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"

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

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

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

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

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

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")
// Compose
Column(Modifier.semantics { paneTitle = "Payment history" }) { /* ... */ }

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" />

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>

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.

來源:DEV Community · dev.to