HealthKit (13)
Accessibility(손쉬운사용) 사용하기
참고 링크를 한번 확인해보자/
ChartContainer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var titleView: some View {
VStack(alignment: .leading) {
// 생략
}
.accessibilityAddTraits(.isHeader)
.accessibilityLabel(accessibilityLabel)
.accessibilityElement(children: .ignore)
}
var accessibilityLabel: String {
switch chartType {
case .stepBar(let average):
"Bar chart, step count, last 28 days, average steps per day: \(average) steps."
case .stepWeekdayPie:
"Pie chart, average steps per weekday."
case .weightLine(let average):
"Line chart, weight, average weight: \(average.formatted(.number.precision(.fractionLength(1)))) pounds, goal weight: 155 pounds"
case .weightDiffBar:
"Bar chart, average weight difference per weekday."
}
}
여기서 .accessibilityElement(children: .ignore)
를 사용하면 titleView의 자식뷰(titleView에 있는 Vstack 영역들)에 대해선 VoiceOver를 통해 siri가 대답하는걸 포함시키지 않겠다는것.
하지만 실행해서 테스트 해보니 titleView에 대한 내용을 언급하지 않았다.
그래서 이리저리 테스트를 해본결과
1
2
3
4
5
6
7
8
var titleView: some View {
VStack(alignment: .leading) {
// 생략
}
.accessibilityAddTraits(.isHeader)
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
}
먼저 ignore하고 label을 언급하게 하니 작동이 되었다.
이후 navigationLinkView에는 새롭게 추가한 부분만 읽게 했다.
1
2
3
4
5
6
7
8
9
10
11
12
var navigationLinkView: some View {
NavigationLink(value: context) {
HStack {
titleView
Spacer()
Image(systemName: "chevron.right")
}
}
.foregroundStyle(.secondary)
.padding(.bottom, 12)
.accessibilityHint("Tap for data in list view") //new
}
실행영상.
ChartView 접근성 적용
Chart Sample은 Swift로 어떤 차트를 만들 수 있는지 보여주는 사이트이다. 참고하면 좋을듯.
각 차트에 해당하는건 우선 코드로 작성하고 대표적인 코드 예시로 이유를 설명하겠다.
StepBarChart
1
2
3
4
5
6
if !chartData.isEmpty {
RuleMark(y: .value("Average", averageSteps))
.foregroundStyle(.secondary)
.lineStyle(.init(lineWidth: 1, dash: [5]))
.accessibilityHidden(true)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ForEach(chartData) { steps in
Plot { // new
BarMark(
x: .value("Date", steps.date, unit: .day),
y: .value("Steps", steps.value)
)
.foregroundStyle(Color.pink.gradient)
.opacity(rawSelectedDate == nil || steps.date == selectedData?.date ? 1.0 : 0.3)
}
.accessibilityLabel(steps.date.accesibilityDate)
.accessibilityValue("\(Int(steps.value)) step")
}
extension Date {
// 생략
var accesibilityDate: String {
self.formatted(.dateTime.month(.wide).day())
}
}
StepPieChart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ChartContainer(chartType: .stepWeekdayPie) {
Chart {
ForEach(chartData) { weekday in
// 생략
.opacity(selectedWeekday?.date.weekdayInt == weekday.date.weekdayInt ? 1.0 : 0.3 )
.accessibilityLabel(weekday.date.weekdayTitle)
.accessibilityValue("\(Int(weekday.value)) steps")
}
}
}
.chartBackground { proxy in
GeometryReader { geo in
if let plotFrame = proxy.plotFrame {
let frame = geo[plotFrame]
if let selectedWeekday {
VStack {
// 생략
}
.position(x: frame.midX, y: frame.midY)
.accessibilityHidden(true) // new
}
}
}
}
WeightLineChart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RuleMark(y: .value("Goal", 155))
// 생략
ForEach(chartData) { weights in
Plot { // new
AreaMark(
// 생략
)
// 생략
LineMark(
// 생략
)
// 생략
}
.accessibilityLabel(weights.date.accesibilityDate)
.accessibilityValue("\(weights.value.formatted(.number.precision(.fractionLength(1)))) pounds")
}
WeightDiffBarChart
1
2
3
4
5
6
7
8
9
10
ForEach(chartData) { weightDiff in
Plot { // new
BarMark(
// 생략
)
// 생략
}
.accessibilityLabel(weightDiff.date.weekdayTitle)
.accessibilityValue("\(weightDiff.value.formatted(.number.precision(.fractionLength(1)).sign(strategy: .always()))) pounds")
}
정리
1. hidden
1
2
3
4
5
6
if !chartData.isEmpty {
RuleMark(y: .value("Average", averageSteps))
.foregroundStyle(.secondary)
.lineStyle(.init(lineWidth: 1, dash: [5]))
.accessibilityHidden(true)
}
현재 RuleMark는 평균선을 의미하는데, VoiceOver는 설정값이 없으면 모든 Components에 대해 값을 읽기에, 평균선에 대한 정보는 읽을 필요가 없으므로 (titleView에서 언급하기 때문) VoiceOver가 해당 Component자체를 인식하게 하지 않게하여 말하는걸 방지.
2. Plot
1
2
3
4
5
6
7
8
9
10
11
12
ForEach(chartData) { steps in
Plot { // new
BarMark(
x: .value("Date", steps.date, unit: .day),
y: .value("Steps", steps.value)
)
.foregroundStyle(Color.pink.gradient)
.opacity(rawSelectedDate == nil || steps.date == selectedData?.date ? 1.0 : 0.3)
}
.accessibilityLabel(steps.date.accesibilityDate)
.accessibilityValue("\(Int(steps.value)) step")
}
UI상으론 차이는 없다.
Plot은 Chart Content 들을 하나의 Entity로 그룹화하는 Mechansm 이라고 Docs에서는 정의 한다.
그냥 여러 차트를 하나의 그룹으로 관리하는 Container정도로 생각하면 되겠다.
Plot을 사용한 이유는 현재 BarChart의 요소 하나하나 접근해서 x, y값에 대해 이야기 하는것보다.
그룹화를 통해 해당 bar로 VoiceOver가 해당영역으로 갔을때 우리가 설정한 Label과 Value로 대신 읽게하는 것이다.
실행영상.
DataListView 접근성 개선
1
2
3
4
5
6
7
8
9
List(listData.reversed()) { data in
LabeledContent {
Text(data.value, format: .number.precision(.fractionLength(metric == .steps ? 0 : 1)))
} label: {
Text(data.date, format: .dateTime.month(.wide).day().year())
.accessibilityLabel(data.date.accesibilityDate)
}
.accessibilityElement(children: .combine)
}
combine을 사용해서 List를 구성하는 두 Text를 한번에 같이 읽게 한다.
combine을 사용한 경우
combine을 하지 않은 경우.
두 비교 영상을 통해 combine이 어떻게 작동하는지 확인이 가능하다.
이번 포스팅에서는 내용이 좀 짧지만, 어플의 접근성(Accessibility) 기능을 개선하여 사용자 경험을 한 단계 더 끌어올렸다. 위에서 적용한 방식을 활용하면 더 많은 사용자에게 직관적이고 편리한 서비스를 제공할 수 있을 것이다.
Github: Step-Tracker Repository