Loading...
Loading...
Compare original and translation side by side
// ❌ WRONG - No label (VoiceOver says "Button")
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
// ❌ WRONG - Generic label
.accessibilityLabel("Button")
// ❌ WRONG - Reads implementation details
.accessibilityLabel("cart.badge.plus") // VoiceOver: "cart dot badge dot plus"
// ✅ CORRECT - Descriptive label
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
// ✅ CORRECT - With hint for complex actions
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")// ❌ 错误 - 未设置标签(VoiceOver会播报“按钮”)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
// ❌ 错误 - 使用通用标签
.accessibilityLabel("Button")
// ❌ 错误 - 播报实现细节
.accessibilityLabel("cart.badge.plus") // VoiceOver会播报:“cart dot badge dot plus”
// ✅ 正确 - 描述性标签
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
// ✅ 正确 - 为复杂操作添加提示
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")// ✅ CORRECT - Hide decorative images from VoiceOver
Image("decorative-pattern")
.accessibilityHidden(true)
// ✅ CORRECT - Combine multiple elements into one label
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")// ✅ 正确 - 隐藏装饰性图片,避免被VoiceOver识别
Image("decorative-pattern")
.accessibilityHidden(true)
// ✅ 正确 - 将多个元素合并为一个标签
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
// ❌ WRONG - Custom font without scaling
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ CORRECT - SwiftUI semantic styles (auto-scales)
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
// ✅ CORRECT - UIKit semantic styles
label.font = UIFont.preferredFont(forTextStyle: .body)
// ✅ CORRECT - Custom font with scaling
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true// ❌ 错误 - 固定大小,无法缩放
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
// ❌ 错误 - 自定义字体未支持缩放
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ 正确 - SwiftUI语义化样式(自动缩放)
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
// ✅ 正确 - UIKit语义化样式
label.font = UIFont.preferredFont(forTextStyle: .body)
// ✅ 正确 - 支持缩放的自定义字体
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
// ⚠️ ACCEPTABLE - Custom font without scaling (accessibility violation)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ GOOD - Custom size that scales with Dynamic Type
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
// ✅ BEST - Use semantic styles when possible
Text("Headline")
.font(.headline)relativeTo:.title2.largeTitle.title2.title2.title.body.caption.system(size:).relativeTo().dynamicTypeSize()// ❌ 错误 - 固定大小,无法缩放
Text("Price: $19.99")
.font(.system(size: 17))
// ⚠️ 可接受 - 自定义字体未支持缩放(违反无障碍要求)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ 推荐 - 随Dynamic Type缩放的自定义尺寸
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
// ✅ 最佳实践 - 尽可能使用语义化样式
Text("Headline")
.font(.headline)relativeTo:.title2.largeTitle.title2.title2.title.body.caption.system(size:).relativeTo().dynamicTypeSize().largeTitle.title.title2.title3.headline.body.callout.subheadline.footnote.caption.caption2.largeTitle.title.title2.title3.headline.body.callout.subheadline.footnote.caption.caption2// ❌ WRONG - Fixed frame breaks with large text
Text("Long product description...")
.font(.body)
.frame(height: 50) // Clips at large text sizes
// ✅ CORRECT - Flexible frame
Text("Long product description...")
.font(.body)
.lineLimit(nil) // Allow multiple lines
.fixedSize(horizontal: false, vertical: true)
// ✅ CORRECT - Stack rearranges at large sizes
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // Limit maximum size if needed// ❌ 错误 - 固定高度会导致大文本被截断
Text("Long product description...")
.font(.body)
.frame(height: 50) // 大文本尺寸下会被裁剪
// ✅ 正确 - 灵活布局
Text("Long product description...")
.font(.body)
.lineLimit(nil) // 允许多行显示
.fixedSize(horizontal: false, vertical: true)
// ✅ 正确 - 大尺寸下自动重排的布局
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // 可根据需要限制最大尺寸.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge).environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)// ❌ WRONG - Low contrast (1.8:1 - fails WCAG)
Text("Warning")
.foregroundColor(.yellow) // on white background
// ❌ WRONG - Low contrast in dark mode
Text("Info")
.foregroundColor(.gray) // on black background
// ✅ CORRECT - High contrast (7:1+ passes AAA)
Text("Warning")
.foregroundColor(.orange) // or .red
// ✅ CORRECT - System colors adapt to light/dark mode
Text("Info")
.foregroundColor(.primary) // Black in light mode, white in dark
Text("Secondary")
.foregroundColor(.secondary) // Automatic high contrast// ❌ 错误 - 低对比度(1.8:1 - 不符合WCAG标准)
Text("Warning")
.foregroundColor(.yellow) // 白色背景下
// ❌ 错误 - 深色模式下对比度不足
Text("Info")
.foregroundColor(.gray) // 黑色背景下
// ✅ 正确 - 高对比度(7:1+ 符合AAA级)
Text("Warning")
.foregroundColor(.orange) // 或红色
// ✅ 正确 - 系统颜色自动适配明暗模式
Text("Info")
.foregroundColor(.primary) // 亮色模式为黑色,暗色模式为白色
Text("Secondary")
.foregroundColor(.secondary) // 自动保持高对比度// ❌ WRONG - Color alone indicates status
Circle()
.fill(isAvailable ? .green : .red)
// ✅ CORRECT - Color + icon/text
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
// ✅ CORRECT - Respect system preference
if UIAccessibility.shouldDifferentiateWithoutColor {
// Use patterns, icons, or text instead of color alone
}// ❌ 错误 - 仅用颜色表示状态
Circle()
.fill(isAvailable ? .green : .red)
// ✅ 正确 - 颜色+图标/文本
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
// ✅ 正确 - 遵循系统偏好设置
if UIAccessibility.shouldDifferentiateWithoutColor {
// 使用图案、图标或文本替代单一颜色
}// ❌ WRONG - Too small (24x24pt)
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
// ❌ WRONG - Small icon without padding
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
// ✅ CORRECT - Minimum 44x44pt
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
// ✅ CORRECT - Larger icon or padding
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle()) // Expand tap area
.onTapGesture { }
// ✅ CORRECT - UIKit button with edge insets
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
// Total size: icon size + insets ≥ 44x44pt// ❌ 错误 - 尺寸过小(24x24pt)
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
// ❌ 错误 - 小图标未添加内边距
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
// ✅ 正确 - 最小44x44pt
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
// ✅ 正确 - 放大图标或添加内边距
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle()) // 扩大点击区域
.onTapGesture { }
// ✅ 正确 - UIKit按钮添加内边距
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
// 总尺寸:图标尺寸 + 内边距 ≥44x44pt// ❌ WRONG - Targets too close (hard to tap accurately)
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
// ✅ CORRECT - Adequate spacing (8pt minimum, 12pt better)
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}// ❌ 错误 - 目标间距过近(难以准确点击)
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
// ✅ 正确 - 足够的间距(最小8pt,12pt更佳)
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}// ❌ WRONG - Custom gesture without keyboard alternative
.onTapGesture {
showDetails()
}
// No way to trigger with keyboard
// ✅ CORRECT - Button provides keyboard support automatically
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command) // Optional shortcut
// ✅ CORRECT - Custom control with focus support
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}// ❌ 错误 - 自定义手势未提供键盘替代方案
.onTapGesture {
showDetails()
}
// 无法通过键盘触发
// ✅ 正确 - Button自动支持键盘操作
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command) // 可选快捷键
// ✅ 正确 - 支持焦点的自定义控件
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}// ✅ CORRECT - Set initial focus
.focusSection() // Group related controls
.defaultFocus($focus, .constant(true)) // Set default
// ✅ CORRECT - Move focus after action
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}// ✅ 正确 - 设置初始焦点
.focusSection() // 分组相关控件
.defaultFocus($focus, .constant(true)) // 设置默认焦点
// ✅ 正确 - 操作后移动焦点
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}// ❌ WRONG - Always animates (can cause nausea)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
// ❌ WRONG - Parallax scrolling without opt-out
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5) // Parallax
}
}
// ✅ CORRECT - Respect Reduce Motion preference
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0 // Instant
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
// ✅ CORRECT - Simpler animation or cross-fade
if UIAccessibility.isReduceMotionEnabled {
// Cross-fade or instant change
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
// Complex spring animation
withAnimation(.spring()) {
showView = true
}
}// ❌ 错误 - 始终播放动画(可能导致恶心)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
// ❌ 错误 - 视差滚动未提供关闭选项
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5) // 视差效果
}
}
// ✅ 正确 - 遵循Reduce Motion偏好设置
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0 // 直接显示
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
// ✅ 正确 - 使用简化动画或淡入淡出
if UIAccessibility.isReduceMotionEnabled {
// 淡入淡出或直接切换
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
// 复杂弹簧动画
withAnimation(.spring()) {
showView = true
}
}// ✅ CORRECT - Automatic support
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil // Disable animation
}
}// ✅ 正确 - 自动支持Reduce Motion
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil // 禁用动画
}
}// ❌ WRONG - Informative image without label
Image("product-photo")
// ✅ CORRECT - Informative image with label
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
// ✅ CORRECT - Decorative image hidden
Image("background-pattern")
.accessibilityHidden(true)// ❌ 错误 - 信息性图片未设置标签
Image("product-photo")
// ✅ 正确 - 信息性图片添加标签
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
// ✅ 正确 - 隐藏装饰性图片
Image("background-pattern")
.accessibilityHidden(true)// ❌ WRONG - Custom button without button trait
Text("Submit")
.onTapGesture {
submit()
}
// VoiceOver announces as "Submit, text" not "Submit, button"
// ✅ CORRECT - Use Button for button-like controls
Button("Submit") {
submit()
}
// VoiceOver announces as "Submit, button"
// ✅ CORRECT - Custom control with correct trait
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}// ❌ 错误 - 自定义按钮未设置按钮特性
Text("Submit")
.onTapGesture {
submit()
}
// VoiceOver会播报为“Submit,文本”而非“Submit,按钮”
// ✅ 正确 - 对按钮类控件使用Button
Button("Submit") {
submit()
}
// VoiceOver会播报为“Submit,按钮”
// ✅ 正确 - 为自定义控件设置正确特性
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}// ❌ WRONG - Custom slider without accessibility support
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
// Drag gesture only, no VoiceOver support
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
}
}
// ✅ CORRECT - Custom slider with accessibility actions
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}// ❌ 错误 - 自定义滑块未支持无障碍
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
// 仅支持拖拽手势,无VoiceOver支持
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
}
}
// ✅ 正确 - 支持无障碍操作的自定义滑块
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}// ❌ WRONG - State change without announcement
Button("Toggle") {
isOn.toggle()
}
// ✅ CORRECT - State change with announcement
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
// ✅ CORRECT - Automatic state with accessibilityValue
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled")// ❌ 错误 - 状态变化未播报
Button("Toggle") {
isOn.toggle()
}
// ✅ 正确 - 状态变化时播报
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
// ✅ 正确 - 通过accessibilityValue自动播报状态
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled").accessibilityAdjustableAction.accessibilityAdjustableActionAccessibility Testing Completed:
- VoiceOver: All screens tested with VoiceOver enabled
- Dynamic Type: Tested at all size categories
- Color Contrast: Verified 4.5:1 minimum contrast
- Touch Targets: All buttons minimum 44x44pt
- Reduce Motion: Animations respect user preference无障碍测试已完成:
- VoiceOver:所有页面已在VoiceOver开启状态下测试
- Dynamic Type:已测试所有尺寸类别
- 色彩对比度:已验证符合最小4.5:1对比度要求
- 触摸目标:所有按钮均满足44x44pt最小尺寸
- 减少动态效果:动画遵循用户偏好设置"I want to support this design direction, but let me show you Apple's App Store
Review Guideline 2.5.1:
'Apps should support accessibility features such as VoiceOver and Dynamic Type.
Failure to include sufficient accessibility features may result in rejection.'
Here's what we need for approval:
1. VoiceOver labels on all interactive elements
2. Dynamic Type support (can't lock font sizes)
3. 4.5:1 contrast ratio for text, 3:1 for UI
4. 44x44pt minimum touch targets
Let me show where our design currently falls short..."“我想支持这个设计方向,但请允许我展示苹果的App Store审核指南2.5.1:
'应用应支持VoiceOver和Dynamic Type等无障碍功能。未提供足够无障碍功能可能导致审核拒绝。'
我们需要满足以下要求才能通过审核:
1. 所有交互元素添加VoiceOver标签
2. 支持Dynamic Type(不能锁定字体大小)
3. 文本对比度4.5:1,UI元素对比度3:1
4. 触摸目标最小44x44pt
让我展示当前设计不符合要求的地方……”"I can achieve your aesthetic goals while meeting accessibility requirements:
1. VoiceOver labels: Add them programmatically (invisible in UI, required for approval)
2. Dynamic Type: Use layout techniques that adapt (examples from Apple HIG)
3. Contrast: Adjust colors slightly to meet 4.5:1 (I'll show options that preserve brand)
4. Touch targets: Expand hit areas programmatically (visual size stays the same)
These changes won't affect the visual design you're seeing, but they're required
for App Store approval and legal compliance."“我可以在满足无障碍要求的同时实现你的美学目标:
1. VoiceOver标签:通过代码添加(界面不可见,但为审核必填)
2. Dynamic Type:使用自适应布局技巧(参考Apple人机交互指南示例)
3. 对比度:微调颜色以达到4.5:1(我会提供保留品牌风格的选项)
4. 触摸目标:通过代码扩大点击区域(视觉尺寸保持不变)
这些修改不会影响你看到的视觉设计,但它们是App Store审核通过和合规的必要条件。”Slack message to PM + designer:
"Design review decided to proceed with:
- Fixed font sizes (disabling Dynamic Type)
- 38x38pt buttons (below 44pt requirement)
- 3.8:1 text contrast (below 4.5:1 requirement)
Important: These changes violate App Store Review Guideline 2.5.1 and WCAG AA.
This creates three risks:
1. App Store rejection during review (adds 1-2 week delay)
2. ADA compliance issues if user files complaint (legal risk)
3. 15% of potential users unable to use app effectively
I'm flagging this proactively so we can prepare a response plan if rejected."发送给产品经理和设计师的Slack消息:
“设计评审决定采用以下方案:
- 使用固定字体大小(禁用Dynamic Type)
- 按钮尺寸38x38pt(低于44pt要求)
- 文本对比度3.8:1(低于4.5:1要求)
重要提示:这些修改违反了App Store审核指南2.5.1和WCAG AA级标准,带来三个风险:
1. App Store审核拒绝(会导致1-2周的延迟)
2. 如果用户投诉,可能面临ADA合规的法律风险(美国)
3. 15%的潜在用户无法有效使用应用
我提前标记此问题,以便我们在被拒绝时能准备好应对方案。”// ❌ WRONG - Generic labels (will fail re-review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button") // Apple will reject again
// ✅ CORRECT - Descriptive labels (passes review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")// ❌ 错误 - 通用标签(会再次被拒绝)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button") // 苹果会再次拒绝
// ✅ 正确 - 描述性标签(通过审核)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")"Design review decided to proceed with [specific violations].
We understand this creates:
- App Store rejection risk (Guideline 2.5.1)
- Potential 1-2 week delay if rejected
- Need to audit and fix all instances if rejected
Monitoring plan:
- Submit for review with current design
- If rejected, implement proper accessibility (estimated 2-4 hours)
- Have accessibility-compliant version ready as backup"“设计评审决定采用[具体违规方案]。
我们清楚这会带来:
- App Store审核拒绝风险(指南2.5.1)
- 如果被拒绝,可能导致1-2周的延迟
- 如果被拒绝,需要审核并修复所有问题
监控计划:
- 按当前设计提交审核
- 如果被拒绝,实施符合无障碍要求的修复(预估2-4小时)
- 准备好符合无障碍标准的备用版本undefinedundefinedundefinedundefined