iOS Accessibility Agent Skill

repository·main·Indexed 41 days ago

https://github.com/dadederk/ios-accessibility-agent-skill

Guidance and implementation patterns for building accessible SwiftUI and UIKit applications. Covers inclusive design principles, the 'Shift Left' approach, and detailed technical implementations for Dynamic Type, including automatic scaling with text styles, @ScaledMetric, ViewThatFits, and UIFontMetrics.

Tokens
36.7K
Snippets
105
Records
160
Agent score
50%

What's inside iOS Accessibility Agent Skill

  1. Adapt Layouts for Large Text

    main

    When text becomes very large, horizontal layouts (like HStack) often break. Use one of these three patterns to adapt your layout:

    1. Flip Stack Axis: Use AnyLayout to switch between HStackLayout and VStackLayout based on the dynamicTypeSize.
    2. Approach with Group: Use a Group with an if/else block to switch between HStack and VStack.
    3. ViewThatFits: Use ViewThatFits to let SwiftUI automatically pick the layout that fits the available space. This is ideal for local UI blocks and handles complex fallback logic better than simple size thresholds.
    // ViewThatFits example
    ViewThatFits {
        HStack { content } // Try horizontal first
        VStack { content } // Fall back to vertical
    }
  2. Control accessibility traversal with grouping and ordering

    main

    You can control how VoiceOver moves through a view hierarchy using three main techniques:

    1. Group traversal without merging: Set shouldGroupAccessibilityChildren = true on a container. This tells VoiceOver to treat the children as a logical group (traversing them all before moving to the next element outside the container) without merging them into a single element.
    2. Explicit element order: Manually define the focus order by assigning an array of views to the accessibilityElements property.
    3. Hiding elements: Use isAccessibilityElement = false to hide a single view, or accessibilityElementsHidden = true to hide an entire subtree from VoiceOver.
    // Group traversal without merging
    containerView.shouldGroupAccessibilityChildren = true
    
    // Explicit element order
    view.accessibilityElements = [playButton, shareButton, moreOptionsButton]
    
    // Hide a single element
    decorativeImage.isAccessibilityElement = false
    
    // Hide an entire subtree
    backgroundView.accessibilityElementsHidden = true
  3. Don't rely on color alone for information

    main

    Users with color blindness or those using Differentiate Without Color may not perceive color changes. Always supplement color cues with icons, shapes, patterns, or text labels.

    // BAD: Only using color to indicate status
    statusLabel.textColor = status == .error ? .red : .green
    
    // GOOD: Using text/icons + color
    statusLabel.text = status == .error ? "⚠️ Error" : "✓ Success"
    statusLabel.textColor = status == .error ? .systemRed : .systemGreen
  4. Implement Inclusive Design principles

    main

    Inclusive design means considering human diversity from the start of the development process rather than as an afterthought. Follow these three principles:

    1. Recognize exclusion: Actively identify which users are being left out and why.
    2. Solve for one, extend to many: Recognize that solutions for specific disabilities (like curb cuts) often provide benefits to a much larger population.
    3. Learn from diversity: Involve people with disabilities in the design and testing process as much as possible.

    When naming features or options, use neutral, descriptive language. Avoid ability-loaded labels (e.g., instead of 'limited mode', use 'Relaxed timing' or 'Guided mode').

  5. Adapt layouts for large text sizes

    main

    When users enable large accessibility sizes, static layouts often fail. Implement adaptive layout patterns to handle increased text volume:

    • Stack Transformation: Convert horizontal stacks (HStack) into vertical stacks (VStack).
    • Grid Transformation: Convert multi-column grids into single-column layouts.
    • Text Wrapping: Allow more lines of text and ensure elements can wrap without clipping.
    • Large Content Viewer: Note that navigation bars, tab bars, and toolbars do not scale automatically; users rely on the system's Large Content Viewer (triggered by a long-press) to see magnified labels in these areas.
  6. Leverage Cross-Assistive Technology benefits

    main

    When implementing accessibility APIs, focus on standard properties that benefit multiple assistive technologies simultaneously. Implementing these once provides broad coverage across:

    • VoiceOver
    • Voice Control
    • Switch Control
    • Full Keyboard Access
    • Braille displays

    Key APIs to use include accessibilityLabel, accessibilityTraits, and custom actions. A good starting point is VoiceOver, as it covers most fundamental accessibility requirements.

  7. Group elements for easier navigation

    main

    To reduce the number of swipes required for VoiceOver users and decrease cognitive load, group related elements into a single accessibility element.

    In UIKit, set isAccessibilityElement = true on the container and provide a combined label. In SwiftUI, use the .accessibilityElement(children: .combine) modifier.

    // UIKit
    cardView.isAccessibilityElement = true
    cardView.accessibilityLabel = "\(title), \(subtitle)"
    cardView.accessibilityTraits = .button
    
    // SwiftUI
    HStack {
        Image(systemName: "star.fill")
        Text("Favorite")
    }
    .accessibilityElement(children: .combine)
  8. Use accessibility values for variable states

    main

    Use accessibilityValue instead of changing the accessibilityLabel when a component has a dynamic state (like a count or a slider position). This keeps the identity of the element (the label) consistent while communicating the current state (the value).

    Common patterns:

    • Sliders: Instead of just a percentage, use meaningful units like currency (NumberFormatter) or time (DateComponentsFormatter).
    • Toggles: Set the value to "On" or "Off".
    • Badges: Hide the badge itself (isAccessibilityElement = false) and attach its count to the parent element's accessibilityValue.
    // Pattern: Label + Value for badges
    orderButton.accessibilityLabel = "Cart"
    orderButton.accessibilityValue = count > 0 ? "\(count) items" : nil
    numberOfItemsLabel.isAccessibilityElement = false
    
    // Pattern: Meaningful slider values
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    priceSlider.accessibilityValue = formatter.string(from: NSNumber(value: priceSlider.value))
    // VoiceOver says "£450,000" instead of "50%"
  9. Understand Dynamic Type and Text Styles

    main

    Dynamic Type is a system-wide text size control that allows users to scale text from -3 to +5 relative to the default. SwiftUI and UIKit text styles scale proportionally with these settings.

    Standard Text Styles (Default Sizes):

    • Large Title: 34pt
    • Title: 28pt
    • Title 2: 22pt
    • Title 3: 20pt
    • Headline: 17pt (bold)
    • Body: 17pt (base size)
    • Callout: 16pt
    • Subheadline: 15pt
    • Footnote: 13pt
    • Caption: 12pt
    • Caption 2: 11pt

    Users can also enable Larger Accessibility Sizes, which provide five additional sizes beyond the standard Large (from XXL to Accessibility 5).

  10. Group Elements with accessibilityElement(children:)

    main

    Control how VoiceOver interacts with a group of child views using .accessibilityElement(children:):

    • .combine: Automatically merges child labels and properties into one single element. Best for simple text groupings.
    • .ignore: Treats the container as a single element and ignores children. You must then manually set the .accessibilityLabel() and other properties.
    • .contain: The container is a semantic group, but children remain individually accessible.

    Example of creating a single adjustable control from multiple buttons:

    HStack {
        ForEach(1..<6) { value in
            Button { rating = value } label: {
                Image(systemName: value <= rating ? "hand.thumbsup.fill" : "hand.thumbsup")
            }
        }
    }
    .accessibilityElement(children: .ignore)
    .accessibilityLabel("Rating")
    .accessibilityValue("\(rating) thumbs up")
    .accessibilityAdjustableAction { direction in
        switch direction {
        case .increment: rating = min(rating + 1, 5)
        case .decrement: rating = max(rating - 1, 1)
        @unknown default: break
        }
    }
    // Automatic merge
    HStack {
        Text("Price")
        Text("$42.00")
    }
    .accessibilityElement(children: .combine)
    
    // Manual utterance
    HStack {
        Text("Price of")
        Text("$42.00")
    }
    .accessibilityElement(children: .ignore)
    .accessibilityLabel("Price of $42.00")
  11. Manage Accessibility Grouping Patterns

    main

    Grouping related elements helps reduce the number of swipes required for navigation and provides better context. There are three main patterns:

    1. Automatic merge: Children are combined into a single spoken element.
      • SwiftUI: Use .combine.
    2. Manual merge: Treat the group as one single element and provide its own label, value, and traits.
      • UIKit: Use isAccessibilityElement.
    3. Semantic grouping / Grouped traversal: Children remain individually accessible, but VoiceOver traverses the entire group before moving to the next section.
      • SwiftUI: Use .contain.
      • UIKit: Use shouldGroupAccessibilityChildren.

    To customize the specific order in which elements are read, use explicit accessibilityElements ordering.