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 {
content
} else {
content.modifier(effect)
}
}
}
[[ stitchable ]] half4 animatedEffect(
float2 position,
half4 color,
float2 size,
float time,
float motionScale
) {
float adjustedTime = time * motionScale;
float waveAmplitude = 10.0 * motionScale;
}
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 {
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)
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)
static let universal = [
Color(red: 0.0, green: 0.45, blue: 0.70),
Color(red: 0.90, green: 0.62, blue: 0.0),
Color(red: 0.0, green: 0.60, blue: 0.50),
Color(red: 0.95, green: 0.90, blue: 0.25),
Color(red: 0.80, green: 0.40, blue: 0.70),
]
}
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()
app.launchArguments += [
"-UIAccessibilityReduceMotion", "1",
"-UIAccessibilityDarkerSystemColors", "1"
]
app.launch()
let shaderView = app.otherElements["shaderEffect"]
XCTAssertTrue(shaderView.exists)
let screenshot = shaderView.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Shader with Reduce Motion"
add(attachment)
}