Accessibility Audit (iOS)
Generated 2026-05-11 · Costco iOS
Why this matters
Every SwiftUI Image, UIImageView, icon-only Button, and tap-gesture-bearing view that conveys meaning needs an .accessibilityLabel(...) (and ideally an .accessibilityHint(...)). Without it:
- Users with vision impairments — VoiceOver reads "Image" or skips silently. They cannot complete tasks that require interacting with iconographic UI (close, back, navigate, scan, expand).
- Automation tests — XCUITest, Appium, BrowserStack, MagicPod, every iOS automation framework, query elements by
app.buttons["Cart"]— that string is theaccessibilityLabel. Without labels, tests fall back to indices (app.buttons.element(boundBy: 3)) — brittle, breaks on any layout change. - Voice Control — Apple's Voice Control feature lets users drive the app by saying "tap cart". Without an accessibilityLabel, the user has no name to say.
- App Store — Apple's App Review explicitly checks accessibility. Submission can be flagged or rejected when major flows fail VoiceOver.
- Compliance — WCAG 1.1.1, ADA, AODA, Section 508, EN 301 549. Same as Android — missing labels are a direct fail.
Decision matrix — when to label, when to hide
| View purpose | Pattern |
|---|---|
| Conveys information | .accessibilityLabel("Cart count: 3") |
| Acts on tap (icon-only Button / tap gesture) | .accessibilityLabel("Close") + .accessibilityHint("Closes the dialog") + .accessibilityAddTraits(.isButton) if not already a Button |
| Pure decoration | .accessibilityHidden(true) — but only if there's no information content |
| Composite (parent + children all describe same thing) | .accessibilityElement(children: .combine) on the parent + label on the parent |
Audit findings — concrete sites
SwiftUI Image without accessibilityLabel
Total identified: ~136 instances across Costco/Costco/ and Costco-Digital/. Sample of high-impact sites:
| File | Line | Image | Where it appears |
|---|---|---|---|
| Costco/.../WebBrowserNavigationBar.swift | 212 | "chevron.backward" | Web browser back button |
| Costco/.../ShoppingContextView.swift | 147 | navBar back button | Shopping context navigation |
| Costco/.../ShoppingContextDebugUrlView.swift | 40 | "xmark.circle.fill" | Close debug URL view |
| Costco-Digital/.../EnvironmentListSection.swift | 49 | checkmark icon | Selection state in env list |
| Costco-Digital/.../TextBuilderView.swift | 234 | chevronUp/chevronDown | Expand/collapse text section |
| Costco-Digital/.../TextSectionView.swift | 121 | chevronUp/chevronDown | Same expand pattern |
| Costco-Digital/.../CostcoVideoPlayer.swift | 63 | "xmark.circle.fill" | Video player close button |
| Costco-Digital/.../CostcoVideoPlayer.swift | 93 | fullscreen toggle | Fullscreen control |
| Costco-Digital/.../FSANotificationView.swift | 123 | xMarkSymbol | Dismiss FSA notification |
| Costco-Digital/.../RatingFiveStarView.swift | 267 | "star.fill" | Rating star (filled) |
| Costco-Digital/.../RatingFiveStarView.swift | 286 | "star" | Rating star (empty) |
| Costco-Digital/.../TextView.swift | 171 | "xmark" | Close action |
| Costco-Digital/Features/DMC/.../CameraView.swift | 102 | scanner left button | Camera/scanner control |
onTapGesture without accessibility traits
Total identified: ~239 instances. Each is a custom-tappable view that VoiceOver announces as "image" or skips entirely. Sample:
| File | Line | Notes |
|---|---|---|
| Costco/.../ShoppingContextDebugUrlView.swift | 43 | Clear-URL tap action |
| Costco-Digital/.../ChangeRemoteEnvironmentView/EnvironmentListSection.swift | 53 | Environment selection |
| Costco-Digital/.../ComponentFactory.swift | 354 | Generic component handler |
| Costco-Digital/.../TextBuilderView.swift | 244 | Section expand toggle |
| Costco-Digital/.../FeatureCardView.swift | 60 | Feature card tap |
| Costco-Digital/Features/DMC/.../CameraView.swift | 102 | Camera control |
| Costco-Digital/Features/Search/.../ProductListFilterView_v1.swift | 145 | Filter clear |
| Costco-Digital/Features/Geofence/.../PermissionsSetupRequestView.swift | 88 | Close permission request |
| Costco-Digital/Features/Accounts/.../AddShopCardView.swift | 458 | Card expansion |
Tap targets below 44pt minimum (WCAG 2.5.5 + Apple HIG)
Total identified: ~105 sites with explicit frame dimensions below 44pt — Apple's documented minimum for tap targets. Sample:
| File | Line | Frame |
|---|---|---|
| Costco/.../GridMenuSection.swift | 62 | 40×40 pt |
| Costco-Digital/.../ButtonSetView.swift | 33, 39 | 40×33 pt × 2 |
| Costco-Digital/.../TextBuilderView.swift | 318 | 24×24 pt — critically small |
| Costco-Digital/.../HeaderView.swift | 59, 89 | 14×14 pt — well below minimum |
| Costco-Digital/.../CouponItemView.swift | 488 | 20×20 pt |
| Costco-Digital/Features/PDP/.../ImageDetailView.swift | 288 | image-control button below 44pt |
| Costco-Digital/Features/Accounts/.../WalletSettingsView.swift | 385 | add-wallet button |
| Costco-Digital/Features/Geofence/.../PermissionRowView.swift | 72 | permission row icon |
| Costco-Digital/Features/Search/.../ProductListFilterView_v1.swift | 457 | filter checkbox |
accessibilityHidden(true) on potentially meaningful content
~192 sites use .accessibilityHidden(true). Many are correct (decorative chevrons, separators) — but several are on content that carries meaning. Sample of suspicious ones to audit:
| File | Line | Context |
|---|---|---|
| Costco-Digital/.../ImageWithLegendView.swift | 44, 49 | Image + legend hidden — legend likely conveys meaning |
| Costco-Digital/.../HeaderView.swift | 60, 90 | Header icons hidden — context-dependent |
| Costco-Digital/.../CostcoVideoPlayer.swift | 33 | Video thumbnail hidden — should announce title |
| Costco-Digital/.../FeatureIconView.swift | 60 | Feature icon hidden — should describe feature |
| Costco-Digital/.../CouponItemView.swift | 555, 568 | Coupon decoration hidden — verify it's truly decorative |
| Costco-Digital/.../ProductFSAEligibleView.swift | 27 | FSA-eligible badge hidden — but FSA eligibility is meaningful |
| Costco-Digital/.../FSANotificationView.swift | 89 | Notification badge hidden — badge usually carries info |
NSLocalizedString with empty translation comments
~205 sites. Empty comments mean translators have no context for the string — they may translate "Close" with the wrong gendered article in French or wrong word order in Japanese. Sample:
- UserVisibleStringConstants.swift:58, 60 — two different "Close" strings, both
comment: "" - UserVisibleStringConstants.swift:188-192 — warehouse callout actions ("See Warehouse Details", "Set as My Warehouse", "Map It") all without translator context
- UserVisibleStringConstants.swift:469-470 — "day" / "days" without context — translators can't know if it's a count, weekday, etc.
- WarehouseLocatorAPIHandler.swift:147, 192, 237 — error messages without context
Hot files (most violations)
- Costco-Digital/.../ViewComponent/AdView/AdView.swift — multiple Image + accessibility-hidden sites
- Costco-Digital/.../ViewComponent/HeaderView/HeaderView.swift — small frames + hidden icons
- Costco-Digital/.../ProductCardFeature/Views/RatingFiveStarView.swift — every star without label
- Costco/.../UserVisibleStringConstants.swift — 100+ NSLocalizedString sites with empty comments
- Costco-Digital/.../ViewComponent/CouponSet/CouponItemView.swift — multiple hidden + small-frame issues
How fixing this helps automation
iOS automation frameworks query by accessibility identifier or label. The same labels you set for VoiceOver are what XCUITest finds elements by:
| Framework | Before (brittle) | After (resilient) |
|---|---|---|
| XCUITest | app.buttons.element(boundBy: 2).tap() | app.buttons["Close"].tap() |
| Appium | XPath dive into accessibility tree | find_element(AppiumBy.ACCESSIBILITY_ID, "Close") |
| BrowserStack App Live | Pixel coordinates | Accessibility-ID-based recording |
| EarlGrey 2 (Google's iOS UI test) | By view-class + index | grey_accessibilityID(@"Close") |
Pair this with the matching Android contentDescription work and your test team can run the same Appium test against both apps — the accessibility label becomes a cross-platform contract.
Specific patterns to apply
SwiftUI — icon-only Button
// Before
Button(action: dismiss) {
Image(systemName: "xmark.circle.fill")
}
// After
Button(action: dismiss) {
Image(systemName: "xmark.circle.fill")
}
.accessibilityLabel("Close")
.accessibilityHint("Closes this view")
SwiftUI — onTapGesture pattern
// Before
Image("cart")
.onTapGesture { showCart() }
// After
Image("cart")
.onTapGesture { showCart() }
.accessibilityLabel("Cart")
.accessibilityHint("View your shopping cart")
.accessibilityAddTraits(.isButton)
SwiftUI — composite component
// Before — 5 stars, 5 separate elements VoiceOver reads
HStack {
Image(systemName: "star.fill")
Image(systemName: "star.fill")
Image(systemName: "star.fill")
Image(systemName: "star.fill")
Image(systemName: "star")
}
// After — one element, one announcement
HStack {
ForEach(0..<5) { i in
Image(systemName: i < rating ? "star.fill" : "star")
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Rating: \(rating) out of 5 stars")
UIKit — UIImageView
// Before
let icon = UIImageView(image: UIImage(named: "ic_phone"))
// After
let icon = UIImageView(image: UIImage(named: "ic_phone"))
icon.isAccessibilityElement = true
icon.accessibilityLabel = NSLocalizedString("Phone", comment: "Icon indicating warehouse phone number")
icon.accessibilityTraits = .image // or .button if tappable
Migration plan
- Sprint 1 — Add
.accessibilityLabelto the 13 high-impact icon-only Buttons (close, back, dismiss, expand, fullscreen). Localize each label. - Sprint 1 — Sweep the 9 listed
onTapGesturesites; add.accessibilityLabel+.accessibilityHint+.accessibilityAddTraits(.isButton). - Sprint 2 — Audit the ~192
.accessibilityHidden(true)sites. Where the content carries meaning (badges, status icons), replace with composite accessibility (one parent label +children: .ignore). - Sprint 2 — Sweep tap targets below 44pt; add
.frame(minWidth: 44, minHeight: 44)+.contentShape(Rectangle()). - Sprint 3 — Fill in NSLocalizedString comments. Each comment should say where the string appears + the linguistic role (verb / noun / count / etc.).
- Ongoing — Custom SwiftLint rule banning new
Image(systemName:)without an accessibility modifier in the same statement. Snapshot test atenvironment(\.dynamicTypeSize, .accessibility5).
Verification
- Manual VoiceOver pass — turn VoiceOver on (
⌘ Option F5on a connected iPhone), navigate Home → Cart → Checkout-equivalent flow. Every interactive element should be announced with name + role + (where useful) hint. - Accessibility Inspector (free, Xcode) — connect to a running simulator, click Audit. Fix all warnings on critical screens until count = 0.
- UI test rewrite — pick one feature; rewrite XCUITest queries to use
.accessibilityIdentifier. Verify they survive a cosmetic refactor. - Apple Privacy Manifest review — accessibility issues are flagged during App Review. Submit a test build and read the review report.
- SwiftLint baseline — bake-in custom rules; baseline diff must reach zero new violations per PR.