HealthKit (7)
Weight Average Chart
이번에도 이전 Pie Chart와 유사하게 진행을 한다.
하지만 BarChart로 한다.
일별 몸무게 변화량 계산 함수 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ChartMath {
// 생략
static func averageDailyWeightDiffs(for weights: [HealthMetric]) -> [WeekdayChartData] {
var diffValues: [(date: Date, value: Double)] = []
for i in 0..<weights.count {
if i == 0 {
diffValues.append((date: weights[i].date, value: 0))
} else {
let date = weights[i].date
let diff = weights[i].value - weights[i-1].value
diffValues.append((date: date, value: diff))
}
}
// demonstrate
for value in diffValues {
print("\(value.date), \(value.value)")
}
return []
}
}
이떄 for-Loop를 사용하여 계산을 하는데, 처음에 들어오는 값은 i - [i-1]이 불가능 하다. 왜냐 i가 0이기때문.
그래서 다음과 같이 케이스를 분류 한다.
- i 가 0일떄
- 초기값을 넣어주되 값의 차이가 없으므로, 0으로 입력해준다.
- i가 0이 아닐떄
- 당시 기준날짜와 그 전날의 몸무게 차를 입력한다.
- 날짜는 당시 기준날짜로 입력한다.
값을 확인해보기 위해
1
2
3
4
5
6
.task {
await hkManager.fetchStepCount()
await hkManager.fetchWeights()
ChartMath.averageDailyWeightDiffs(for: hkManager.weightData) // new
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
함수를 호출 이전에도 언급했지만 fetch 이후에 호출해야 정상적으로 출력이 된다.
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
26
27
2024-11-16 15:00:00 +0000, 0.0
2024-11-17 15:00:00 +0000, -1.2502482354838094
2024-11-18 15:00:00 +0000, -0.22473774080492603
2024-11-19 15:00:00 +0000, 0.4933261262566475
2024-11-20 15:00:00 +0000, 1.6625963779698338
2024-11-21 15:00:00 +0000, -2.967321923566061
2024-11-22 15:00:00 +0000, -1.0160749065856862
2024-11-23 15:00:00 +0000, -0.5727954176436185
2024-11-24 15:00:00 +0000, 0.3036361963050922
2024-11-25 15:00:00 +0000, 0.745888871890827
2024-11-26 15:00:00 +0000, 0.06974078546724627
2024-11-27 15:00:00 +0000, -0.09375231029005704
2024-11-28 15:00:00 +0000, -1.2151304399167486
2024-11-29 15:00:00 +0000, -2.422474193514148
2024-11-30 15:00:00 +0000, 3.3072745295006314
2024-12-01 15:00:00 +0000, -2.53604214784076
2024-12-02 15:00:00 +0000, -0.42137284854393897
2024-12-03 15:00:00 +0000, 0.6689918838416418
2024-12-04 15:00:00 +0000, -2.3812392249148218
2024-12-05 15:00:00 +0000, 2.9388034667839804
2024-12-06 15:00:00 +0000, -1.0216868447327556
2024-12-07 15:00:00 +0000, -0.9525480231076244
2024-12-08 15:00:00 +0000, 0.8232329626256387
2024-12-09 15:00:00 +0000, 0.5873847039588327
2024-12-10 15:00:00 +0000, -1.3608451591123583
2024-12-11 15:00:00 +0000, -2.0446019972211786
2024-12-12 15:00:00 +0000, 2.2166057559194314
이제 이렇게 정리 된 값에 대해서 각 요일별로 나누기 위해 chunk
를 다시 사용한다.
1
2
3
4
5
6
7
8
9
10
let sortedByWeekday = diffValues.sorted { $0.date.weekdayInt < $1.date.weekdayInt }
let weekdayArray = sortedByWeekday.chunked { $0.date.weekdayInt == $1.date.weekdayInt }
// demonstrate
for array in weekdayArray {
print("-----")
for day in array{
print("\(day.date.weekdayInt), \(day.value)")
}
}
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
26
27
28
29
30
31
32
33
34
-----
1, 0.0
1, -0.5727954176436185
1, 3.3072745295006314
1, -0.9525480231076244
-----
2, -1.2502482354838094
2, 0.3036361963050922
2, -2.53604214784076
2, 0.8232329626256387
-----
3, -0.22473774080492603
3, 0.745888871890827
3, -0.42137284854393897
3, 0.5873847039588327
-----
4, 0.4933261262566475
4, 0.06974078546724627
4, 0.6689918838416418
4, -1.3608451591123583
-----
5, 1.6625963779698338
5, -0.09375231029005704
5, -2.3812392249148218
5, -2.0446019972211786
-----
6, -2.967321923566061
6, -1.2151304399167486
6, 2.9388034667839804
6, 2.2166057559194314
-----
7, -1.0160749065856862
7, -2.422474193514148
7, -1.0216868447327556
이렇게 일요일 부터 순서대로 분류가 된걸 알 수 있다.
Steps, Weight 마지막 날짜 불일치 문제 확인
이때 이전과 달리 7이 3개 인데, 아이러니하게도 14일 데이터가 빠져있다.
둘다 첫데이터는 11.16으로 동일하지만 마지막은 하루 차이가난다.
Simulator를 아예 리셋을 하고 다시 데이터를 넣어서 테스트를 해본다.
리셋은 여기서 한다.
문제는 아래에 적었고, 코드에만 집중하면
let endDate = Calendar.current.date(byAdding: .day, value: 0, to: startDate)!
여기를 1대신 0으로 수정한다. 여기서 현재보다 하루가 더 추가되어서 된 문제로 파악
이젠 제대로 된다.
리셋 후 Device 삭제 에러 발생
여러번 테스트를 하며 리셋을 하다가
이런 에러가 발생
DevCleaner라는 앱을 사용하여 전부 리셋.
그리고 재실행 했으나
새로운 에러 발생 이번엔
아예 Xcode내 시뮬레이터를 새로 설치하기로 결정.
그래도 에러는 동일하다.
테스트결과 주로 사용중인 16.Pro 시뮬레이터에서만 에러가 발생하는걸 확인.
여기서 삭제가 안되어서 finder로 직접 경로를 찾아 해당 시뮬레이터 디바이스 삭제
이후 새롭게 생성.
작동 확인 완료.
다시 돌아와서, 이전에 만든 코드를 그대로 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var weekdayChartData: [WeekdayChartData] = []
for array in weekdayArray {
guard let firstValue = array.first else { continue }
let total = array.reduce(0) { $0 + $1.value }
let avgSteps = total/Double(array.count)
weekdayChartData.append(.init(date: firstValue.date, value: avgSteps))
}
// demonstrate
for data in weekdayChartData {
print("\(data.date.weekdayInt), \(data.value)")
}
1
2
3
4
5
6
7
1, 0.06774690627121771
2, -0.7455018709098908
3, -1.5871069626733103
4, 1.4432189328259568
5, -2.2668498306830784
6, 0.4853978600464117
7, 0.26445627570351604
이부분은 과정만 간단하게 적어봤다. 자세한 부연설명은 이전글 참고.
몸무게 변화량 fetch 함수 만들기.
지금은 몸무게를 가져오는 함수를 가져와서 처리를 했는데,
몸무게 데이터를 가져와 일별 변화량을 계산하고 이를 기반으로 요일별 평균 차이를 구하는 함수를 만든다.
기존 fetchWeights 함수와 유사하지만, 이번에는 몸무게 변화량을 구하는 함수로 수정한다.
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
26
27
var weightDiffData: [HealthMetric] = []
func fetchWeightsForDifferentials() async {
let calendar = Calendar.current
let today = calendar.startOfDay(for: .now)
let endDate = calendar.date(byAdding: .day, value: 1, to: today)!
let startDate = calendar.date(byAdding: .day, value: -29, to: endDate) // modified 28 to 29
let queryPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
let samplePredicate = HKSamplePredicate.quantitySample(type: HKQuantityType(.bodyMass), predicate: queryPredicate)
let weightQuery = HKStatisticsCollectionQueryDescriptor(predicate: samplePredicate,
options: .mostRecent,
anchorDate: endDate,
intervalComponents: .init(day: 1)
)
do {
let weights = try! await weightQuery.result(for: store)
weightDiffData = weights.statistics().map({
.init(date: $0.startDate, value: $0.mostRecentQuantity()?.doubleValue(for: .pound()) ?? 0)
})
} catch {
}
}
이제 가져온 데이터를 기반으로 일별 변화량을 계산한다. 글 초반에 만들어둔 함수를 보강했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static func averageDailyWeightDiffs(for weights: [HealthMetric]) -> [WeekdayChartData] {
var diffValues: [(date: Date, value: Double)] = []
var weekdayChartData: [WeekdayChartData] = []
for i in 1 ..< weights.count { // modified
let date = weights[i].date
let diff = weights[i].value - weights[i-1].value
diffValues.append((date: date, value: diff))
}
let sortedByWeekday = diffValues.sorted { $0.date.weekdayInt < $1.date.weekdayInt }
let weekdayArray = sortedByWeekday.chunked { $0.date.weekdayInt == $1.date.weekdayInt }
for array in weekdayArray {
guard let firstValue = array.first else { continue }
let total = array.reduce(0) { $0 + $1.value }
let avgSteps = total/Double(array.count)
weekdayChartData.append(.init(date: firstValue.date, value: avgSteps))
}
return weekdayChartData
}
기존의 if 문을 없애 주었다.
우선 let startDate = calendar.date(byAdding: .day, value: -29, to: endDate)
를 우리는 28일 전 데이터를 가져오는데, 그것보다 하루 더 전 데이터를 가져와서 처리한다.
print를 해보면 다음과 같이 나온다.
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
2024-11-18 15:00:00 +0000, -4.810411545336507
2024-11-19 15:00:00 +0000, 1.277945432356546
2024-11-20 15:00:00 +0000, 1.0977315675878572
2024-11-21 15:00:00 +0000, 0.6202349150307782
2024-11-22 15:00:00 +0000, -3.430152365958037
2024-11-23 15:00:00 +0000, 2.9206005833777624
2024-11-24 15:00:00 +0000, -3.929184130558383
2024-11-25 15:00:00 +0000, -0.08349774157275647
2024-11-26 15:00:00 +0000, 2.3254008514163047
2024-11-27 15:00:00 +0000, -3.924448825541731
2024-11-28 15:00:00 +0000, 0.6352072169949281
2024-11-29 15:00:00 +0000, 2.8371124637994853
2024-11-30 15:00:00 +0000, -1.1796375984609426
2024-12-01 15:00:00 +0000, -1.215148440148198
2024-12-02 15:00:00 +0000, 0.06867529782479664
2024-12-03 15:00:00 +0000, 0.693210730426415
2024-12-04 15:00:00 +0000, -3.444848692993787
2024-12-05 15:00:00 +0000, 0.46814173975184303
2024-12-06 15:00:00 +0000, 2.007966949778478
2024-12-07 15:00:00 +0000, -3.010440625641536
2024-12-08 15:00:00 +0000, 2.1623250870670176
2024-12-09 15:00:00 +0000, -1.5231938616087746
2024-12-10 15:00:00 +0000, 1.4763187171045615
2024-12-11 15:00:00 +0000, -2.795833371784653
2024-12-12 15:00:00 +0000, 0.21800756840809754
2024-12-13 15:00:00 +0000, -0.3571019448058621
2024-12-14 15:00:00 +0000, 1.540465265809587
-----
1, 2.9206005833777624
1, -1.1796375984609426
1, -3.010440625641536
1, 1.540465265809587
-----
2, -3.929184130558383
2, -1.215148440148198
2, 2.1623250870670176
-----
3, -4.810411545336507
3, -0.08349774157275647
3, 0.06867529782479664
3, -1.5231938616087746
-----
4, 1.277945432356546
4, 2.3254008514163047
4, 0.693210730426415
4, 1.4763187171045615
-----
5, 1.0977315675878572
5, -3.924448825541731
5, -3.444848692993787
5, -2.795833371784653
-----
6, 0.6202349150307782
6, 0.6352072169949281
6, 0.46814173975184303
6, 0.21800756840809754
-----
7, -3.430152365958037
7, 2.8371124637994853
7, 2.007966949778478
7, -0.3571019448058621
-----
1, 0.06774690627121771
2, -0.9940024945465211
3, -1.5871069626733103
4, 1.4432189328259568
5, -2.2668498306830784
6, 0.4853978600464117
7, 0.26445627570351604
순서대로 과정을 출력을 쭉 해보았다. 상세 로직에 대한 부연설명은 이전글 참고
현재 시뮬레이터에 추가하는 로직을 정리과 위의 함수와 함수결과를 정리를 해본다.
addSimulator
함수를 통해서 시뮬레이터에 임의의 값을 추가- 이때
0..<28
을 사용하여 현재기준 28일 전까지의 데이터를 생성한다.
- 이때
fetchWeightsForDifferentials
함수를 통해 29일 전까지의 데이터를 가져온다.- 29일 전 데이터는 추가하지 않았으므로 0이 된다.
averageDailyWeightDiffs
를 통해 weight를 한달동안 각 요일별 변화량의 평균을 계산한다.
Average Weight Change View 만들기
Average Weight Change View는 요일별 체중 변화량을 시각적으로 보여주는 뷰로, 각 요일의 체중 증감치를 양수는 Indigo, 음수는 Teal 색상으로 나타내며 이를 BarChart를 통해 표현한다.
우선 여기부분은 강의에서 직접 만들어보라고 했기에 직접 만들어 본다.
만들어야 디자인은 위와 같다.
여기부분은 내가 직접 하면서 약간 애매했거나, 막힌 부분을 정리를 해본다.
우선 생각은 다음과 같다.
- 기존에 했던 코드와 유사하되 BarMark를 사용하면 될걸로 보인다.
- MockData를 새로 하나 더 만든다.
- MockData를 적용하여 Preview를 통해 UI를 확인하며 코드를 수정한다.
Step 1
기존에 WeightChart의 코드를 가져와서 필요한 부분만 수정한다.
1
2
3
4
BarMark(
x: .value("Date", weights.date),
y: .value("Diff", weights.value)
)
여긴 이부분이 포인트. 다시 BarChart를 사용.
Step 2
1
2
3
4
5
6
7
8
9
10
11
static var weightDiffs: [WeekdayChartData] {
var array: [WeekdayChartData] = []
for i in 0..<7 {
let diff = WeekdayChartData(date: Calendar.current.date(byAdding: .day, value: -i, to: .now)!,
value: .random(in: -4...4))
array.append(diff)
}
return array
}
이렇게 임의 값으로 각 요일에 따른 차이 값을 구현한다.
Step 3
현재 Preview를 보면 다음과 같다.
지금 필요한 부분은 크게 4가지로 보인다.
- Bar의 두께(width) 변경
- AxisMark에 모든 요일 표시
- 0보다 큰수는 indigo, 음수는 teal 색상 사용 (gradient 적용)
- Annotation View 바꾸기.
1. Bar 두께 변경
현재는 우리가 의도 한 부분과 전혀 다른 Bar의 width가 나왔다.
Docs 를 보면 width가 있다.
1
2
3
4
5
6
7
ForEach(chartData) { weights in
BarMark(
x: .value("Date", weights.date),
y: .value("Diff", weights.value),
width: 20 // new
)
}
이걸 수치로 정해주면 width가 변하긴 한다.
하지만 정해진 범위를 벗어나는 문제와 Annotation을 보여줄떄 그래프도 튀는것을 알 수 있다.
- 범위를 벗어나는 Bar
- Annotation시, Bar UI 튐 현상.
여기는 도저히 아이디어가 안떠올라서 강의를 참고한다.
1. 범위를 벗어나는 Bar
1
2
3
4
5
6
7
8
9
10
11
// My Code
BarMark(
x: .value("Date", weight.date),
y: .value("Diff", weight.value)
)
// Lecture Code
BarMark(
x: .value("Date", weight.date, unit: .day),
y: .value("Diff", weight.value)
)
바로 unit 저것의 차이였다…..
바로 해결이 되었다.
Unit을 사용하는 이유는 참고글에 따르면 unit값을 그룹화 하고자 함이다.
여기선 일별로 그룹화를 진행하였다.
즉 지금은 위에서 보면 값이 2024-12-01 15:00:00 +0000, -2.53604214784076
이런식으로 존재하는데,
이걸 unit을 사용하여 2024-12-01, -2.53604214784076
이런식으로 시간은 무시하고 그 날짜에 해당하는 값으로 그룹화를 한다는 것.
이부분은 나중에 시간별로 다른 값을 넣어서 시간이 width에 영향이 있는지 확인을 해볼 예정.
2. Annotation시, Bar UI 튐 현상.
1번의 Case로 같이 해결
2. AxisMark에 모든 요일 표시
그냥 이렇게 하면 되겠지 하고 했는데 되었다.
Docs를 읽다보면 거의 끝부분에 stride에 대한 예시 코드도 있으니 한번 참고 하면 좋을듯 하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// My Code
.chartXAxis {
AxisMarks(values: .stride(by: .day)) {
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
// Lecture Code
.chartXAxis {
AxisMarks(values: .stride(by: .day)) {
AxisValueLabel(format: .dateTime.weekday(), centered: true)
}
}
이때 포인트는 stride를 사용하여 나눈다.
stride는 Docs에서 정의하길
Creates values with the given calendar unit.
주어진 달력 단위로 값을 만든다.
여기선 일 단위로 나눈다라는것.
그리고 강의에선 centered
를 사용하여 가운데에 딱 맞췄다. 위의 사진과 비교하면 살짝 불일치 하는것을 보이는데, 가운데 정렬을 함으로써 Bar와 Label을 맞춰주었다.
3. 양수, 음수에 따른 다른 색상 적용
1
2
3
4
5
BarMark(
x: .value("Date", weights.date, unit: .day),
y: .value("Diff", weights.value)
)
.foregroundStyle(weights.value > 0 ? Color.indigo.gradient : Color.teal.gradient)
이렇게 삼항연산자를 적용하면 색이 바뀐다
Step 4
여기선 크게 3가지로 나뉘어질듯하다.
- 위와 같이 양수, 음수에 따른 색상적용
- 날짜대신 요일로 변경
- 수치 앞에 +, - 표시 (현재는 -만 나옴)
1. 양수, 음수에 따른 색상적용
1
2
3
Text(selectedData?.value ?? 0, format: .number.precision(.fractionLength(2)))
.fontWeight(.heavy)
.foregroundStyle((selectedData?.value ?? 0) > 0 ? Color.indigo : Color.mint) // modified
2. 날짜대신 요일로 변경
1
2
3
Text(selectedData?.date ?? .now, format: .dateTime.weekday(.wide)) // modified
.font(.footnote.bold())
.foregroundStyle(.secondary)
3. 수치 앞에 +, - 표시
1
2
3
Text(selectedData?.value ?? 0, format: .number.sign(strategy: .always()).precision(.fractionLength(2))) // modified
.fontWeight(.heavy)
.foregroundStyle((selectedData?.value ?? 0) > 0 ? Color.indigo : Color.mint)
이전에 언급했던 참고글이 생각나서 foramt에 대해 보던 중 필요한 부분이 있어 적용한다.
이렇게 직접 해보았는데, 내가 디테일한 부분을 놓치고 있었다는걸 다시 한번 알게 된 좋은 경험이었다.
DashBoardView에 적용하기
이렇게 View를 만들었으니 이제 DashBoardView에 적용을 해본다.
1
2
3
case .weight:
WeightLineChartView(selectedStat: selectedStat, chartData: hkManager.weightData)
WeightDiffBarChartView(chartData: hkManager.weightDiffData)
이렇게 사용하게 되면 Type이 맞지 않아 에러가 발생한다.
그래서 위에서 미리 만들어둔 averageDailyWeightDiffs
함수를 사용한다.
1
2
3
case .weight:
WeightLineChartView(selectedStat: selectedStat, chartData: hkManager.weightData)
WeightDiffBarChartView(chartData: ChartMath.averageDailyWeightDiffs(for: hkManager.weightDiffData)) // modified
그러면 weight를 가져와서 순서대로 처리를 하여 우리가 원하는 각 요일마다의 값으로 반환을 해줄것이다.
이때 차이값을 fetch하기 위해 위에서 만들어둔 fetchWeightsForDifferentials
를 호출해줘야한다.
1
2
3
4
5
6
.task {
await hkManager.fetchStepCount()
await hkManager.fetchWeights()
await hkManager.fetchWeightsForDifferentials() // new
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
실행하면 다음과 같다.
Github: Step-Tracker Repository