Overview › Accessibility Audit (iOS)

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:

Decision matrix — when to label, when to hide

View purposePattern
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:

FileLineImageWhere it appears
Costco/.../WebBrowserNavigationBar.swift212"chevron.backward"Web browser back button
Costco/.../ShoppingContextView.swift147navBar back buttonShopping context navigation
Costco/.../ShoppingContextDebugUrlView.swift40"xmark.circle.fill"Close debug URL view
Costco-Digital/.../EnvironmentListSection.swift49checkmark iconSelection state in env list
Costco-Digital/.../TextBuilderView.swift234chevronUp/chevronDownExpand/collapse text section
Costco-Digital/.../TextSectionView.swift121chevronUp/chevronDownSame expand pattern
Costco-Digital/.../CostcoVideoPlayer.swift63"xmark.circle.fill"Video player close button
Costco-Digital/.../CostcoVideoPlayer.swift93fullscreen toggleFullscreen control
Costco-Digital/.../FSANotificationView.swift123xMarkSymbolDismiss FSA notification
Costco-Digital/.../RatingFiveStarView.swift267"star.fill"Rating star (filled)
Costco-Digital/.../RatingFiveStarView.swift286"star"Rating star (empty)
Costco-Digital/.../TextView.swift171"xmark"Close action
Costco-Digital/Features/DMC/.../CameraView.swift102scanner left buttonCamera/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:

FileLineNotes
Costco/.../ShoppingContextDebugUrlView.swift43Clear-URL tap action
Costco-Digital/.../ChangeRemoteEnvironmentView/EnvironmentListSection.swift53Environment selection
Costco-Digital/.../ComponentFactory.swift354Generic component handler
Costco-Digital/.../TextBuilderView.swift244Section expand toggle
Costco-Digital/.../FeatureCardView.swift60Feature card tap
Costco-Digital/Features/DMC/.../CameraView.swift102Camera control
Costco-Digital/Features/Search/.../ProductListFilterView_v1.swift145Filter clear
Costco-Digital/Features/Geofence/.../PermissionsSetupRequestView.swift88Close permission request
Costco-Digital/Features/Accounts/.../AddShopCardView.swift458Card 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:

FileLineFrame
Costco/.../GridMenuSection.swift6240×40 pt
Costco-Digital/.../ButtonSetView.swift33, 3940×33 pt × 2
Costco-Digital/.../TextBuilderView.swift31824×24 pt — critically small
Costco-Digital/.../HeaderView.swift59, 8914×14 pt — well below minimum
Costco-Digital/.../CouponItemView.swift48820×20 pt
Costco-Digital/Features/PDP/.../ImageDetailView.swift288image-control button below 44pt
Costco-Digital/Features/Accounts/.../WalletSettingsView.swift385add-wallet button
Costco-Digital/Features/Geofence/.../PermissionRowView.swift72permission row icon
Costco-Digital/Features/Search/.../ProductListFilterView_v1.swift457filter 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:

FileLineContext
Costco-Digital/.../ImageWithLegendView.swift44, 49Image + legend hidden — legend likely conveys meaning
Costco-Digital/.../HeaderView.swift60, 90Header icons hidden — context-dependent
Costco-Digital/.../CostcoVideoPlayer.swift33Video thumbnail hidden — should announce title
Costco-Digital/.../FeatureIconView.swift60Feature icon hidden — should describe feature
Costco-Digital/.../CouponItemView.swift555, 568Coupon decoration hidden — verify it's truly decorative
Costco-Digital/.../ProductFSAEligibleView.swift27FSA-eligible badge hidden — but FSA eligibility is meaningful
Costco-Digital/.../FSANotificationView.swift89Notification 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:

Hot files (most violations)

  1. Costco-Digital/.../ViewComponent/AdView/AdView.swift — multiple Image + accessibility-hidden sites
  2. Costco-Digital/.../ViewComponent/HeaderView/HeaderView.swift — small frames + hidden icons
  3. Costco-Digital/.../ProductCardFeature/Views/RatingFiveStarView.swift — every star without label
  4. Costco/.../UserVisibleStringConstants.swift — 100+ NSLocalizedString sites with empty comments
  5. 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:

FrameworkBefore (brittle)After (resilient)
XCUITestapp.buttons.element(boundBy: 2).tap()app.buttons["Close"].tap()
AppiumXPath dive into accessibility treefind_element(AppiumBy.ACCESSIBILITY_ID, "Close")
BrowserStack App LivePixel coordinatesAccessibility-ID-based recording
EarlGrey 2 (Google's iOS UI test)By view-class + indexgrey_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

  1. Sprint 1 — Add .accessibilityLabel to the 13 high-impact icon-only Buttons (close, back, dismiss, expand, fullscreen). Localize each label.
  2. Sprint 1 — Sweep the 9 listed onTapGesture sites; add .accessibilityLabel + .accessibilityHint + .accessibilityAddTraits(.isButton).
  3. 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).
  4. Sprint 2 — Sweep tap targets below 44pt; add .frame(minWidth: 44, minHeight: 44) + .contentShape(Rectangle()).
  5. Sprint 3 — Fill in NSLocalizedString comments. Each comment should say where the string appears + the linguistic role (verb / noun / count / etc.).
  6. Ongoing — Custom SwiftLint rule banning new Image(systemName:) without an accessibility modifier in the same statement. Snapshot test at environment(\.dynamicTypeSize, .accessibility5).

Verification

Costco iOS · Code Review Report · Generated 2026-05-11 · 88 machine-curated findings