Appendix C: Accessibility Checklist

Inclusive shader design: Motion preferences, color blindness, high contrast, screen reader compatibility, and testing strategies.

Motion Preferences Handling

Respecting Reduce Motion

struct MotionSafeShader: ViewModifier {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    let effect: ShaderEffect
    
    func body(content: Content) -> some View {
        if reduceMotion {
            // Static version or disabled effect
            content
        } else {
            // Full animated effect
            content.modifier(effect)
        }
    }
}

// In Metal shader
[[ stitchable ]] half4 animatedEffect(
    float2 position,
    half4 color,
    float2 size,
    float time,
    float motionScale  // 0.0 for reduced motion, 1.0 for full
) {
    // Scale all motion by motionScale
    float adjustedTime = time * motionScale;
    float waveAmplitude = 10.0 * motionScale;
    
    // Effect still visible but static when motionScale = 0
}

Alternative Static Versions

enum EffectMode {
    case animated
    case reduced
    case static
    
    init(accessibility: AccessibilityPreferences) {
        if accessibility.reduceMotion && accessibility.reduceTransparency {
            self = .static
        } else if accessibility.reduceMotion {
            self = .reduced
        } else {
            self = .animated
        }
    }
}

Color Blindness Considerations

Color-Safe Palettes

struct ColorBlindSafePalette {
    // Deuteranopia-safe (red-green blindness, most common)
    static let safeRed = Color(red: 0.9, green: 0.3, blue: 0.1)
    static let safeGreen = Color(red: 0.1, green: 0.6, blue: 0.3)
    
    // Protanopia-safe
    static let distinctBlue = Color(red: 0.1, green: 0.3, blue: 0.9)
    static let distinctYellow = Color(red: 0.9, green: 0.8, blue: 0.1)
    
    // Universal safe set (works for all types)
    static let universal = [
        Color(red: 0.0, green: 0.45, blue: 0.70),  // Blue
        Color(red: 0.90, green: 0.62, blue: 0.0),  // Orange
        Color(red: 0.0, green: 0.60, blue: 0.50),  // Green
        Color(red: 0.95, green: 0.90, blue: 0.25), // Yellow
        Color(red: 0.80, green: 0.40, blue: 0.70), // Purple
    ]
}

Contrast Enhancement Shader

[[ stitchable ]] half4 enhanceContrast(
    float2 position,
    half4 color,
    float2 size,
    float contrastBoost  // 1.0 to 3.0
) {
    // Convert to luminance
    float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
    
    // Enhance contrast around middle gray
    float enhanced = (luminance - 0.5) * contrastBoost + 0.5;
    enhanced = saturate(enhanced);
    
    // Preserve some color while boosting contrast
    half3 result = mix(half3(enhanced), color.rgb, 0.3);
    
    return half4(result, color.a);
}

High Contrast Support

Detecting High Contrast Mode

struct HighContrastAware: ViewModifier {
    @Environment(\.colorSchemeContrast) private var contrast
    
    func body(content: Content) -> some View {
        content
            .visualEffect { view, proxy in
                view.colorEffect(
                    ShaderLibrary.adaptiveEffect(
                        .float2(proxy.size),
                        .float(contrast == .increased ? 2.0 : 1.0)
                    )
                )
            }
    }
}

High Contrast Shader Modifications

[[ stitchable ]] half4 highContrastBorder(
    float2 position,
    half4 color,
    float2 size,
    float borderWidth,
    float contrastMode  // 0 = normal, 1 = high contrast
) {
    float2 uv = position / size;
    
    // Increase border width in high contrast mode
    float adjustedWidth = borderWidth * (1.0 + contrastMode);
    
    // Use pure black/white in high contrast mode
    half3 borderColor = contrastMode > 0.5 
        ? half3(0.0)  // Pure black
        : half3(0.2, 0.3, 0.4);  // Subtle gray
    
    // Sharper edges in high contrast
    float edge = contrastMode > 0.5
        ? step(adjustedWidth, min(uv.x, uv.y))
        : smoothstep(0.0, adjustedWidth, min(uv.x, uv.y));
        
    return half4(mix(borderColor, color.rgb, edge), color.a);
}

Screen Reader Compatibility

Descriptive Labels for Effects

struct AccessibleShaderView: View {
    @State private var effectIntensity: Float = 0.5
    
    var body: some View {
        Image("photo")
            .visualEffect { view, proxy in
                view.colorEffect(
                    ShaderLibrary.artisticBlur(
                        .float2(proxy.size),
                        .float(effectIntensity)
                    )
                )
            }
            .accessibilityLabel("Photo with artistic blur effect")
            .accessibilityValue("Blur intensity: \(Int(effectIntensity * 100)) percent")
            .accessibilityHint("Swipe up or down to adjust blur intensity")
            .accessibilityAdjustableAction { direction in
                switch direction {
                case .increment:
                    effectIntensity = min(1.0, effectIntensity + 0.1)
                case .decrement:
                    effectIntensity = max(0.0, effectIntensity - 0.1)
                @unknown default:
                    break
                }
            }
    }
}

Testing Strategies

Accessibility Testing Checklist

  • [ ] Test with Reduce Motion enabled
  • [ ] Test with Increase Contrast enabled
  • [ ] Test with each color filter (Settings > Accessibility > Display & Text Size > Color Filters)
  • [ ] Test with VoiceOver navigation
  • [ ] Test with Switch Control
  • [ ] Verify minimum touch target size (44x44 points)
  • [ ] Check contrast ratios (4.5:1 for normal text, 3:1 for large text)
  • [ ] Test with Display Zoom enabled
  • [ ] Verify no seizure-inducing patterns (< 3 flashes per second)

Automated Testing

func testShaderAccessibility() throws {
    let app = XCUIApplication()
    
    // Enable accessibility features
    app.launchArguments += [
        "-UIAccessibilityReduceMotion", "1",
        "-UIAccessibilityDarkerSystemColors", "1"
    ]
    
    app.launch()
    
    // Verify shader effect is visible but not animated
    let shaderView = app.otherElements["shaderEffect"]
    XCTAssertTrue(shaderView.exists)
    
    // Take screenshot for manual verification
    let screenshot = shaderView.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.name = "Shader with Reduce Motion"
    add(attachment)
}