feat(a11y): VoiceOver labels for icon-only buttons + combined action-cell narration
접근성 감사 ③ — 전 화면의 아이콘 전용 버튼과 텍스트 없는 시각
요소에 보이스오버 라벨 부여 (시각 UI 변화 없음):
- 모음 탭: 행동 버튼 셀을 label(이름)+value(측정 중/오늘 누적)로
한 덩어리 낭독 — 아이콘·타이머가 따로 읽히지 않음. 진행 중 행의
정지 버튼 "측정 종료".
- 기록 탭: 날짜 이동 화살표(이전/다음 날짜), 내보내기, 기록 필터,
필터 칩 해제 버튼. 타임테이블의 색 블록·횟수 점은 "행동 이름 +
시각(구간)" verbatim 라벨로 낭독 가능하게.
- 통계 탭: 기간 이동 화살표(이전/다음 기간), 내보내기, 통계 필터,
필터 해제.
- 행동/꼬리표/목표 탭: + 추가 버튼(행동/꼬리표/목표 추가), ⋯ 메뉴.
- 일기: 달 이동 화살표(이전/다음 달), 내보내기, ⋯ 메뉴, 기분 타일
("오늘 기분"), 할 일 삭제 버튼.
- ⑥ 현재 진행 중 위젯: 종료 버튼 "측정 종료".
- CLAUDE.md §15에 컨벤션 추가: 아이콘 전용 버튼 accessibilityLabel
필수, 복합 셀은 label+value 한 덩어리, 텍스트 없는 시각 요소는
verbatim 라벨.
신규 문구 9종 ko/en/ja 번역 + 위젯 카탈로그 '측정 종료' 번역 복사
(전 카탈로그 missing 0/stale 0). Debug·Store 빌드 성공, 접근성
수식어는 시각 렌더링에 영향 없음(스모크 런치 확인). 실기기에서
보이스오버 켜고 핵심 흐름(행동 시작/종료·날짜 이동) 낭독 확인 권장.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
747aab109d
commit
9c9c24f7df
@ -330,3 +330,4 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
|
||||
7. 화면과 내보내기가 같은 값을 보여야 하는 곳은 ExportBuilder 스냅숏을 공유할 것 (일기 요약이 그 예)
|
||||
8. 위젯에서 흰색 하드코딩 금지 — `widgetRenderingMode` 분기 (§10)
|
||||
9. 시뮬레이터 검증 스크린샷은 `xcrun simctl io <sim> screenshot`, 런치 인자는 콜드 스타트로
|
||||
10. **아이콘 전용 버튼에는 `accessibilityLabel` 필수** (보이스오버가 "버튼"으로만 읽는 것 방지). 여러 요소로 된 셀은 label+value로 한 덩어리 낭독(모음 탭 행동 셀 참고), 타임테이블 블록·점처럼 텍스트 없는 시각 요소는 verbatim 라벨(이름+시각)로
|
||||
|
||||
@ -2863,6 +2863,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"다음 기간": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Next period"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "次の期間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"다음 날짜": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Next day"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "次の日"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"다음 달": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Next month"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "次の月"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"다이나믹 아일랜드 · 잠금화면": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -3926,6 +3974,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"메뉴": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Menu"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "メニュー"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"메뉴 닫기": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -6766,6 +6830,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"오늘 %lld회": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%lld times today"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "今日%lld回"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"오늘 0분": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -7839,6 +7919,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"이전 기간": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Previous period"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "前の期間"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"이전 날짜": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Previous day"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "前の日"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"이전 달": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Previous month"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "前の月"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"이하 유지": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -10289,6 +10417,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"필터 해제": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Clear filter"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "フィルターを解除"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"필터와 하루 기준": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
@ -92,6 +92,7 @@ struct ActionListView: View {
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
.accessibilityLabel(Text("메뉴"))
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
@ -103,6 +104,7 @@ struct ActionListView: View {
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibilityLabel(Text("행동 추가"))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAdd) {
|
||||
|
||||
@ -129,6 +129,7 @@ struct DiaryRootView: View {
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.accessibilityLabel(Text("일기 내보내기"))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingExport) {
|
||||
@ -214,6 +215,7 @@ struct DiaryRootView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("이전 달"))
|
||||
Spacer()
|
||||
Text(monthAnchor.formatted(.dateTime.year().month()))
|
||||
.font(.title3.weight(.bold))
|
||||
@ -225,6 +227,7 @@ struct DiaryRootView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("다음 달"))
|
||||
Button("이번 달") {
|
||||
monthAnchor = math.dayKey(for: .now)
|
||||
}
|
||||
@ -603,6 +606,7 @@ struct DiaryDetailView: View {
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
.accessibilityLabel(Text("메뉴"))
|
||||
}
|
||||
|
||||
private func pager(_ entry: DiaryEntry) -> some View {
|
||||
@ -1751,6 +1755,7 @@ private struct DiaryMoodCard: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(readOnly)
|
||||
.accessibilityLabel(Text("오늘 기분"))
|
||||
}
|
||||
|
||||
private var emojiPicker: some View {
|
||||
@ -1884,6 +1889,7 @@ private struct DiaryTodoRow: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("삭제"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +137,7 @@ struct GoalListView: View {
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibilityLabel(Text("목표 추가"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,7 @@ struct HistoryView: View {
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.accessibilityLabel(Text("내보내기"))
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
@ -133,6 +134,7 @@ struct HistoryView: View {
|
||||
? "line.3.horizontal.decrease.circle.fill"
|
||||
: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
.accessibilityLabel(Text("기록 필터"))
|
||||
}
|
||||
}
|
||||
.sheet(item: $editingSession) { session in
|
||||
@ -220,6 +222,7 @@ struct HistoryView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("필터 해제"))
|
||||
}
|
||||
.foregroundStyle(AppTheme.green)
|
||||
.padding(.leading, 10)
|
||||
@ -242,6 +245,7 @@ struct HistoryView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("이전 날짜"))
|
||||
Spacer()
|
||||
Button {
|
||||
showingCalendar = true
|
||||
@ -283,6 +287,7 @@ struct HistoryView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("다음 날짜"))
|
||||
Button("오늘") {
|
||||
selectedDayKey = math.dayKey(for: .now)
|
||||
}
|
||||
@ -649,6 +654,9 @@ struct TimetableView: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 색 블록만으로는 보이스오버가 읽을 내용이 없다 — 행동 이름과 시간대를 라벨로
|
||||
.accessibilityLabel(Text(verbatim:
|
||||
"\(item.action.name) \(Format.time(item.range.lowerBound)) ~ \(Format.time(item.range.upperBound))"))
|
||||
.frame(width: max(width - 4, 8), height: height)
|
||||
.offset(x: 2, y: y)
|
||||
}
|
||||
@ -674,6 +682,8 @@ struct TimetableView: View {
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text(verbatim:
|
||||
"\(item.action.name) +\(item.entry.amount) \(Format.time(item.entry.timestamp))"))
|
||||
.offset(x: min(x, width - size), y: max(y - size / 2, 0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -846,6 +846,7 @@ struct RunningSessionRow: View {
|
||||
.foregroundStyle(AppTheme.yellow)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("측정 종료"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -906,6 +907,19 @@ struct ActionButtonCell: View {
|
||||
.buttonStyle(PressableButtonStyle())
|
||||
.sensoryFeedback(.impact(weight: .medium), trigger: bounce)
|
||||
.disabled(isEditing)
|
||||
// 보이스오버: 셀 전체를 "이름 + 오늘 값" 한 덩어리로 읽는다 (아이콘·타이머 개별 낭독 방지)
|
||||
.accessibilityLabel(Text(action.name))
|
||||
.accessibilityValue(Text(accessibilityValueText))
|
||||
}
|
||||
|
||||
/// 보이스오버용 현재 상태 문구 (측정 중이면 상태, 아니면 오늘 누적)
|
||||
private var accessibilityValueText: String {
|
||||
switch action.trackingType {
|
||||
case .time:
|
||||
return action.isRunning ? String(localized: "측정 중") : todayLabel
|
||||
case .count:
|
||||
return String(localized: "오늘 \(todayCount)회")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@ -87,6 +87,7 @@ struct StatsTabView: View {
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.accessibilityLabel(Text("내보내기"))
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
@ -96,6 +97,7 @@ struct StatsTabView: View {
|
||||
? "line.3.horizontal.decrease.circle.fill"
|
||||
: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
.accessibilityLabel(Text("통계 필터"))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingFilter) {
|
||||
@ -150,6 +152,7 @@ struct StatsTabView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("이전 기간"))
|
||||
Spacer()
|
||||
VStack(spacing: 1) {
|
||||
Text(periodLabel)
|
||||
@ -168,6 +171,7 @@ struct StatsTabView: View {
|
||||
.frame(width: 44, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel(Text("다음 기간"))
|
||||
Button(currentPeriodLabel) {
|
||||
anchorDayKey = math.dayKey(for: .now)
|
||||
}
|
||||
@ -247,6 +251,7 @@ struct StatsTabView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("필터 해제"))
|
||||
}
|
||||
.foregroundStyle(AppTheme.green)
|
||||
.padding(.leading, 10)
|
||||
|
||||
@ -74,6 +74,7 @@ struct TagListView: View {
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibilityLabel(Text("꼬리표 추가"))
|
||||
}
|
||||
}
|
||||
.sheet(item: $editingTag) { tag in
|
||||
|
||||
@ -2649,6 +2649,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"측정 종료": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Stop"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "計測終了"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"측정 중": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
@ -216,6 +216,7 @@ struct NowRunningStopButton: View {
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("측정 종료"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user