All skills
dpearson2699 avatar

/ios-accessibility

@45c9085

Build and audit SwiftUI, UIKit, and AppKit accessibility for VoiceOver, Voice Control, Switch Control, Full Keyboard Access, Dynamic Type, focus restoration, labels/traits/actions, traversal, custom rotors, NSAccessibility, XCTest checks, adaptive system preferences, and App Store accessibility declarations. Use when implementing accessible UI, fixing an accessibility audit, testing assistive-technology behavior, or substantiating App Store Accessibility Nutrition Labels.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/ios-accessibility

This session only. Nothing lands on disk.

referencesa11y-patterns.md

≈3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Accessibility Patterns Reference

Contents

  • Labels, Values, and Hints
  • Traits and Element Grouping
  • Custom Controls and Adjustable Actions
  • Focus Management Patterns
  • Dynamic Type and Layout
  • Custom Rotors
  • System Accessibility Preferences
  • UIKit Accessibility Patterns
  • AppKit Accessibility Patterns
  • Voice Control Patterns
  • Switch Control Patterns
  • Full Keyboard Access Patterns
  • Automated Accessibility Testing

Labels, Values, and Hints

Button(action: { }) {
    Image(systemName: "heart.fill")
}
.accessibilityLabel("Favorite")

Slider(value: $volume, in: 0...100)
    .accessibilityValue("\(Int(volume)) percent")

Button("Submit")
    .accessibilityHint("Submits the form and sends your feedback")

Traits and Element Grouping

// Add traits without overwriting defaults
Button("Go") { }
    .accessibilityAddTraits(.updatesFrequently)

// Group children into a single accessibility element
HStack {
    Image(systemName: "person.circle")
    VStack {
        Text("John Doe")
        Text("Engineer")
    }
}
.accessibilityElement(children: .combine)

// Binary custom control: prefer Toggle when possible; otherwise expose toggle state
HStack {
    Image(systemName: isFavorite ? "heart.fill" : "heart")
    Text(product.name)
}
.onTapGesture { isFavorite.toggle() }
.accessibilityElement()
.accessibilityLabel("Favorite \(product.name)")
.accessibilityValue(isFavorite ? "On" : "Off")
.accessibilityAddTraits(.isToggle)
.accessibilityAction { isFavorite.toggle() }

Custom Controls and Adjustable Actions

HStack { /* custom star rating UI */ }
    .accessibilityElement()
    .accessibilityLabel("Rating")
    .accessibilityValue("\(rating) out of 5 stars")
    .accessibilityAdjustableAction { direction in
        switch direction {
        case .increment: if rating < 5 { rating += 1 }
        case .decrement: if rating > 1 { rating -= 1 }
        @unknown default: break
        }
    }

For custom quantity controls, steppers, ratings, sliders, or other adjustable values, prefer the native control first. If the control is custom, SwiftUI needs .accessibilityAdjustableAction; UIKit custom controls also need accessibilityTraits.insert(.adjustable).

Focus Management Patterns

@AccessibilityFocusState private var focusOnTrigger: Bool

Button("Open Settings") { showSheet = true }
    .accessibilityFocused($focusOnTrigger)
    .sheet(isPresented: $showSheet) {
        SettingsSheet()
            .onDisappear {
                Task { @MainActor in
                    try? await Task.sleep(for: .milliseconds(100))
                    focusOnTrigger = true
                }
            }
    }
enum A11yFocus: Hashable { case nameField, emailField, submitButton }
@AccessibilityFocusState private var focus: A11yFocus?

Dynamic Type and Layout

@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .body) private var rowSpacing: CGFloat = 12
@ScaledMetric(relativeTo: .body) private var controlHeight: CGFloat = 44
@Environment(\.dynamicTypeSize) var dynamicTypeSize

var body: some View {
    Group {
        if dynamicTypeSize.isAccessibilitySize {
            VStack(alignment: .leading) { icon; textContent }
        } else {
            HStack { icon; textContent }
        }
    }
    .frame(minHeight: controlHeight)
}

Use @ScaledMetric(relativeTo:) for non-text dimensions that need to track text size, including icon sizes, spacing, control heights, and custom hit-region dimensions.

Custom Rotors

List(items) { item in ItemRow(item: item) }
    .accessibilityRotor("Unread") {
        ForEach(items.filter { !$0.isRead }) { item in
            AccessibilityRotorEntry(item.title, id: item.id)
        }
    }

System Accessibility Preferences

@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast
@Environment(\.legibilityWeight) var legibilityWeight

UIKit Accessibility Patterns

customButton.accessibilityTraits.insert(.button)
customButton.accessibilityTraits.remove(.staticText)

UIAccessibility.post(notification: .announcement, argument: "Upload complete")
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)

AppKit Accessibility Patterns

AppKit accessibility centers on NSAccessibilityProtocol. Use standard AppKit controls when possible, then override or add accessibility behavior only where the default metadata is wrong or incomplete.

Custom NSView with role, label, value, and action

final class FavoriteToggleView: NSView {
    var isFavorite = false {
        didSet {
            NSAccessibility.post(element: self, notification: .valueChanged)
        }
    }

    override func isAccessibilityElement() -> Bool { true }
    override func accessibilityRole() -> NSAccessibility.Role? { .button }
    override func accessibilityLabel() -> String? { "Favorite" }
    override func accessibilityValue() -> Any? { isFavorite ? "On" : "Off" }

    override func accessibilityPerformPress() -> Bool {
        isFavorite.toggle()
        return true
    }
}

NSAccessibilityElement for non-view items

Use NSAccessibilityElement when an accessible item has no backing NSView, such as a virtual data point in a chart or a drawn annotation.

let pointElement = NSAccessibilityElement.element(
    withRole: .button,
    frame: pointFrame,
    label: "March revenue",
    parent: chartView
)

AppKit announcements and notifications

NSAccessibility.post(element: saveStatusLabel, notification: .valueChanged)

NSAccessibility.post(
    element: self,
    notification: .announcementRequested,
    userInfo: [
        .announcement: "Export complete"
    ]
)

Use .announcementRequested when assistive apps need to announce transient status. Use state-specific notifications such as .valueChanged when the accessible value changed.

Voice Control Patterns

Voice Control generates tap targets from accessibility labels. Labels must be speakable and unique within the visible screen.

accessibilityInputLabels (iOS 14+)

Provide shorter spoken alternatives when the primary label is long, awkward, repeated, localized, acronym-heavy, or commonly shortened in speech:

// Primary label is descriptive but long to speak
Button(action: { startWorkout() }) {
    VStack {
        Image(systemName: "figure.run")
        Text("Start Outdoor Running Workout")
    }
}
.accessibilityLabel("Start Outdoor Running Workout")
.accessibilityInputLabels(["Start Run", "Run", "Start Workout"])
// Navigation link with verbose label
NavigationLink {
    AccountSettingsView()
} label: {
    Label("Account and Privacy Settings", systemImage: "person.circle")
}
.accessibilityInputLabels(["Account", "Settings", "Privacy"])

Also consider input labels for repeated row actions, quantity controls, media controls, and product-name labels where Voice Control users are likely to speak a shorter command than the visible text.

Speakable Label Guidelines

// Bad: emoji-only, unspeakable
Button("❤️") { toggleFavorite() }

// Good: speakable label
Button(action: { toggleFavorite() }) {
    Image(systemName: "heart.fill")
}
.accessibilityLabel("Favorite")

// Bad: duplicate labels on same screen
ForEach(items) { item in
    Button("Edit") { edit(item) }  // Voice Control can't distinguish
}

// Good: unique labels
ForEach(items) { item in
    Button("Edit") { edit(item) }
        .accessibilityLabel("Edit \(item.name)")
}

Switch Control Patterns

Switch Control scans elements sequentially. Reduce scan stops with grouping and provide custom actions for gesture-based interactions.

Custom Actions for Gesture Alternatives

// Swipe-to-delete row: Switch Control can't swipe
TaskRow(task: task)
    .accessibilityAction(named: "Complete") { completeTask(task) }
    .accessibilityAction(named: "Delete") { deleteTask(task) }
    .accessibilityAction(named: "Reschedule") { rescheduleTask(task) }
// Long-press context menu: expose actions directly
PhotoThumbnail(photo: photo)
    .contextMenu { /* ... */ }
    .accessibilityAction(named: "Share") { sharePhoto(photo) }
    .accessibilityAction(named: "Add to Album") { addToAlbum(photo) }
    .accessibilityAction(named: "Delete") { deletePhoto(photo) }

Grouping for Scan Efficiency

// Bad: 5 scan stops per row
HStack {
    Image(systemName: "doc")
    VStack {
        Text(document.title)
        Text(document.date.formatted())
    }
    Spacer()
    Text(document.size)
    Image(systemName: "chevron.right")
}

// Good: 1 scan stop per row
HStack {
    Image(systemName: "doc")
    VStack {
        Text(document.title)
        Text(document.date.formatted())
    }
    Spacer()
    Text(document.size)
    Image(systemName: "chevron.right")
}
.accessibilityElement(children: .combine)

Full Keyboard Access Patterns

Full Keyboard Access review checks whether keyboard users can complete the same common tasks without touch. Keep the implementation mechanics in the focus-engine skill when the fix requires Tab-order wiring, skipped custom cards, .focusable(), SwiftUI keyboard focus state, focus sections, directional movement, tvOS focus, or UIFocusGuide.

  • Every interactive control is reachable by keyboard.
  • Activation works with expected keyboard input.
  • Focus indicators are visible and not hidden by custom styling.
  • Focus traversal is logical and does not trap users in a region.
  • Gesture-only interactions have keyboard-operable alternatives.
  • App shortcuts do not override system shortcuts.
  • Custom controls skipped by Tab should be filed as keyboard focus implementation issues and routed to focus-engine; keep the accessibility finding here.
  • Explicitly assess traversal impact: accessibility element order and grouping affect VoiceOver swipe order, Switch Control scan order, Voice Control overlay targeting, and Full Keyboard Access reachability review.

Automated Accessibility Testing

Use XCUIElement attributes to verify accessibility properties in UI tests.

Verifying Labels and Identifiers

func testAccessibilityLabels() throws {
    let app = XCUIApplication()
    app.launch()

    // Verify buttons have meaningful labels
    let settingsButton = app.buttons["Settings"]
    XCTAssertTrue(settingsButton.exists, "Settings button must exist")
    XCTAssertTrue(settingsButton.isEnabled, "Settings button must be enabled")

    // Verify a cell groups content correctly
    let productCell = app.cells.element(boundBy: 0)
    XCTAssertFalse(productCell.label.isEmpty, "Product cell must have a combined label")
}

Testing Focus and Selection State

func testTabNavigationOrder() throws {
    let app = XCUIApplication()
    app.launch()

    let usernameField = app.textFields["Username"]
    let passwordField = app.secureTextFields["Password"]

    usernameField.tap()
    XCTAssertTrue(usernameField.hasFocus)

    // Tab to next field
    usernameField.typeText("\t")
    XCTAssertTrue(passwordField.hasFocus)
}

Testing Custom Actions

func testSwipeToDeleteAlternative() throws {
    let app = XCUIApplication()
    app.launch()

    let cell = app.cells["task-buy-groceries"]
    XCTAssertTrue(cell.exists)
    try app.performAccessibilityAudit(for: [.action])

    // Verify the custom gesture has a discoverable, operable alternative.
    let deleteButton = app.buttons["Delete Buy groceries"]
    XCTAssertTrue(deleteButton.exists)
    deleteButton.tap()
    XCTAssertFalse(cell.exists)
}

The audit and behavioral assertion complement, but do not replace, manual VoiceOver and Switch Control testing of the custom action.

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides comprehensive guidance and code patterns for implementing accessibility features in iOS and macOS applications using SwiftUI, UIKit, and AppKit. It covers VoiceOver, Voice Control, Switch Control, and Dynamic Type, along with App Store declaration requirements. No security vulnerabilities or malicious patterns were detected.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer6mo

    1/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 45c9085. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 3 months ago
  • voiceover
  • swiftui
  • uikit
  • accessibility
  • ios
  • macos
  • keyboard-access
  • dynamic-type
  • focus-management
  • assistive-technologies

README badge

README badge for dpearson2699/swift-ios-skills/ios-accessibility

Implements VoiceOver, Voice Control, Switch Control, and Full Keyboard Access support in iOS/macOS apps with SwiftUI and UIKit. Covers accessibility labels, focus management, Dynamic Type, custom rotors, and system preference handling (Reduce Motion, Increase Contrast, Bold Text). Includes patterns for testing accessibility compliance and auditing assistive technology support.

Generated from the current SKILL.md.

Does this skill cover both SwiftUI and UIKit?
Yes. The skill provides patterns for SwiftUI accessibility modifiers, focus management with @AccessibilityFocusState, and Dynamic Type support, plus separate UIKit patterns for trait modification, modal dialogs, and accessibility notifications.
What assistive technologies does this cover?
VoiceOver, Voice Control, Switch Control, Full Keyboard Access, and Assistive Access (iOS 18+). The skill also covers focus management, custom rotors, and system accessibility preferences like Reduce Motion and Increase Contrast.
Does this include testing and auditing guidance?
Yes. The skill references automated accessibility testing with XCTest and includes a review checklist for auditing a11y compliance, plus common mistakes to avoid.
Can I use this skill if I'm working with custom controls?
Yes. The skill covers custom control patterns including trait assignment, custom actions for Switch Control, custom rotors, and focus management with @AccessibilityFocusState for sheet and modal dismissals.
Does this support Dynamic Type and system preferences?
Yes. The skill includes @ScaledMetric patterns for Dynamic Type, and covers respecting system accessibility preferences: Reduce Motion, Reduce Transparency, Bold Text, and Increased Contrast.

Generated from the current SKILL.md. These answers refresh after source changes.