HealthKit (3)
Simulator에서 MockData가 필요한 이유
- 시뮬레이터 환경 제약:
- HealthKit은 시뮬레이터에서 기본적으로 데이터를 수집하거나 저장할 수 없다.
- 데이터를 입력하거나 확인할 수 있는 기능이 제한적이기 때문에 MockData를 통해 시뮬레이터 환경에서도 동작을 검증할 수 있다.
- 수동 데이터 입력의 번거로움 제거:
- Health 앱에서는 데이터를 수동으로 추가할 수 있지만, 하루 단위로 데이터를 하나씩 입력하는 것은 매우 번거롭고 시간이 오래 걸린다.
- MockData 생성을 통해 대량의 데이터를 자동으로 추가함으로써 이러한 번거로움을 없애고 효율적인 개발 환경을 제공한다.
- 테스트 자동화와 반복 가능성:
- MockData는 다양한 테스트 시나리오를 시뮬레이션하는 데 유용하며, 반복 가능한 데이터 세트를 제공한다.
- 특정 날짜 범위의 데이터 생성, 다양한 조건에서의 동작 검증 등도 손쉽게 수행할 수 있다.
- 실제 데이터 사용의 한계:
- HealthKit은 사용자 데이터를 다루기 때문에 테스트 시 실제 데이터를 사용하는 것은 보안 및 개인 정보 보호 측면에서 적절하지 않다.
- MockData를 사용하면 민감한 데이터를 포함하지 않고 안전하게 테스트할 수 있다.
MockData 생성
HealthKitManager에 시뮬레이터에서 테스트할 MockData를 추가하는 함수를 만들어본다.
이때 우리가 CoreData를 사용하듯 save를 사용하는데 첫번째는 단일 객체, 두번째는 집합인데, 우리는 걸음 수, 몸무게 이렇게 2개를 사용하기에 집합인 두번째를 사용하여 만든다.
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
func addSimulatorData() async {
var mockSamples: [HKQuantitySample] = []
for i in 0..<28 {
let stepQuantity = HKQuantity(unit: .count(), doubleValue: .random(in: 4_000...20_000))
let weightQuantity = HKQuantity(unit: .pound(), doubleValue: .random(in: (160 + Double(i/3)...165 + Double(i/3))))
let startDate = Calendar.current.date(byAdding: .day, value: -i, to: .now)!
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: .now)!
let stepSample = HKQuantitySample(
type: HKQuantityType(.stepCount),
quantity: stepQuantity,
start: startDate,
end: endDate
)
let weightSample = HKQuantitySample(
type: HKQuantityType(.bodyMass),
quantity: weightQuantity,
start: startDate,
end: endDate
)
mockSamples.append(stepSample)
mockSamples.append(weightSample)
}
try! await store.save(mockSamples)
print("✅ Dummy Data sent up")
}
HKquantitySample은 여러 파라미터들을 받지만 여기서는 4개를 사용했다.
type: The type of sample to be created.
quantity: The value to be stored in the sample.
start: The start date for the sample.
end: The end date for the sample.
from Docs
날짜는 startDate의 경우 현재를 기준으로 i일 씩 감소, 즉 하루씩 계속 감소하기에 과거의 날짜가 계속 생성이 된다. endDate는 value에 1이므로 그냥 현재 날짜를 그대로 가져온다.
이렇게 되면 stepSample, weightSample에는 현재부터해서 for Loop의 범위만큼 과거의 날짜까지의 걸음수, 체중의 값이 배열에 등록이 된다.
이후 DashBoardView에 onAppear 대신 task로 바꿔준다.
1
2
3
4
5
6
@Environment(HealthKitManager.self) private var hkManager
.task {
await hkManager.addSimulatorData()
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
왜냐 addSimulator 함수가 async가 있는 비동기 함수이기때문.
그리고 실행하니 발생하는 에러
HKQuantityTypeIdentifierStepCount 샘플 유형에 대해 시작 날짜(startDate)와 종료 날짜(endDate) 간의 차이가 최대 허용 기간(345,600초, 즉 4일)을 초과했음을 나타낸다. Apple의 HealthKit은 HKQuantityTypeIdentifierStepCount에 대해 단일 샘플의 기간을 4일로 제한한다.
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
이렇게 now에서 startDate로 바꿔준다.
이렇게 하게되면 그날 그날의 샘플데이터가 생성이 된다.
이건 앱을 설치하고 HealthKit과 연동을 허가안했을때 발생하는 에러.
이럴땐 task에 있는 샘플 데이터 호출함수를 잠시 주석을 잡고 실행해서 허가를 한 뒤에 다시 주석을 풀고 실행하면 된다.
이렇게 임의의 값이 추가된것을 알 수 있다.
데이터를 생성한뒤에는 호출함수를 주석을 잡아주자.
계속 데이터를 새로바꾸면서 할필요는 없기때문이다.
Fetch HealthKit Data
Docs에 HealthKit으로 부터 데이터를 읽는 내용이 있으니 읽어보자.
Docs에 Queries가 있는데 이건 아래에 별도로 서술.
우리는 Docs에 있는 여러 Query중 Queries의 Statistics collection query
를 사용한다.
예시코드를 기반으로 함수를 작성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func fetchStepCount() 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: -28, to: endDate)
let queryPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
let samplePredicate = HKSamplePredicate.quantitySample(type: HKQuantityType(.stepCount), predicate: queryPredicate)
let stepsQuery = HKStatisticsCollectionQueryDescriptor(predicate: samplePredicate,
options: .cumulativeSum,
anchorDate: endDate,
intervalComponents: .init(day: 1)
let stepsCounts = try! await stepsQuery.result(for: store)
for steps in stepsCounts.statistics() {
print(steps.sumQuantity() ?? 0)
}
}
날짜 section
첫 4개의 상수는 날짜 관련이다. (첫번째는 현재의 캘린더를 나타내기에 패스)
1
2
3
4
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: -28, to: endDate)
- 캘린더: 현재 캘린더를 사용한다.
- today: 현재 날짜의 시작 시점. 오늘 하루의 기준점을 만든다.
- endDate: 다음 날의 시작 시점. 날짜 범위의 종료점을 설정한다.
- startDate: 종료 날짜 기준으로 28일 전의 날짜. MockData를 생성할 때 28일치 데이터를 만들었으므로, 동일한 날짜 범위를 설정한다.
Predicate Section
1
2
let queryPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
let samplePredicate = HKSamplePredicate.quantitySample(type: HKQuantityType(.stepCount), predicate: queryPredicate)
샘플데이터에 대한 쿼리를 만들어 준다.
queryPredicate
: HealthKit 샘플 데이터에서 특정 날짜 범위를 필터링하기 위한 조건을 생성한다.- startDate와 endDate 사이의 데이터를 가져오도록 설정.
samplePredicate
: 날짜 범위를 기준으로 특정 데이터 타입(여기서는 stepCount)에 대한 조건을 추가한다.- 샘플 데이터를 가져올 때, 특정 타입의 데이터를 필터링하여 반환.
Predicate 적용
1
2
3
4
5
6
let stepsQuery = HKStatisticsCollectionQueryDescriptor(predicate: samplePredicate,
options: .cumulativeSum,
anchorDate: endDate,
intervalComponents: .init(day: 1))
let stepsCounts = try! await stepsQuery.result(for: store)
stepsQuery
생성:- samplePredicate를 사용하여 HealthKit 데이터에서 걸음 수 데이터를 필터링.
- options: .cumulativeSum으로 데이터의 총합을 계산.
- anchorDate는 통계 계산을 시작하는 기준 날짜.
- intervalComponents는 하루 단위(day: 1)로 데이터를 그룹화.
stepsCounts
실행:- stepsQuery.result(for: store)를 호출하여 지정된 조건에 따라 데이터를 HealthKit에서 가져온다.
이후 동일하게 몸무게를 가져오는것도 만들어준다.
단지 바뀐점이라면
1
2
3
4
5
6
7
8
9
func fetchWeights() async {
// 생략...
let samplePredicate = HKSamplePredicate.quantitySample(type: HKQuantityType(.bodyMass), predicate: queryPredicate)
let weightQuery = HKStatisticsCollectionQueryDescriptor(predicate: samplePredicate,
options: .mostRecent,
anchorDate: endDate,
intervalComponents: .init(day: 1))
}
type에 몸무게인 bodyMass를 사용한것과, options에서 mostRecent를 사용했다는 것. 그날 몸무게중 가장 최근에 측정한 몸무게를 가져온다. (왜냐면 실제로 몸무게를 당일에 여러번 잴수도 있으니까.)
마지막엔 DashboardView의 task modifier에 추가
1
2
3
4
5
.task {
//await hkManager.addSimulatorData()
await hkManager.fetchStepCount()
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
1
2
3
4
5
6
7
8
9
10
4453.69 count
28118.3 count
44028.5 count
42475 count
39613.9 count
41009.8 count
42455.9 count
38918.5 count
50162.1 count
// 후략..
이렇게 출력이 되는걸 알 수 있다.
HealthKit Queries 정리
일반 Queries
HealthKit은 데이터를 가져오기 위해 다양한 타입의 쿼리를 제공하며, 모든 쿼리는 백그라운드 큐에서 실행된다. 쿼리 실행 완료 시 결과 핸들러가 실행된다.
1. Sample Query
- 설명: 일반적인 샘플 데이터를 가져오는 쿼리.
- 특징: 정렬 또는 반환 샘플 수 제한 가능.
- 참고:
HKSampleQueryDescriptor
.
2. Anchored Object Query
- 설명: HealthKit 스토어의 변경 사항 검색.
- 특징:
- 첫 실행 시 모든 매칭 샘플 반환.
- 이후 실행 시 추가 또는 삭제된 항목만 반환.
- 참고:
HKAnchoredObjectQueryDescriptor
.
3. Statistics Query
- 설명: 데이터 세트의 합계, 최소, 최대 또는 평균 값을 계산.
- 참고:
HKStatisticsQueryDescriptor
.
4. Statistics Collection Query
- 설명: 고정된 시간 간격으로 여러 통계 계산.
- 특징: 주로 그래프 생성에 사용.
- 참고:
HKStatisticsCollectionQueryDescriptor
.
5. Correlation Query
- 설명: 복잡한 데이터 검색.
- 특징: 개별 샘플 타입에 대한 조건 설정 가능.
- 참고:
HKCorrelationQuery
.
6. Source Query
- 설명: 데이터를 저장한 소스(앱, 디바이스) 검색.
- 참고:
HKSourceQueryDescriptor
.
7. Activity Summary Query
- 설명: 사용자의 하루 또는 특정 기간의 활동 요약 검색.
- 참고:
HKActivitySummaryQueryDescriptor
.
8. Document Query
- 설명: 건강 문서를 검색.
- 참고:
HKDocumentQuery
.
Long-Running Queries
HealthKit은 지속적으로 백그라운드에서 실행하며 스토어 변경 사항을 앱에 업데이트하는 쿼리를 제공한다.
1. Observer Query
- 설명: HealthKit 스토어의 변경 사항을 모니터링하고 알림 제공.
- 특징: 백그라운드 알림 등록 가능.
- 참고:
HKObserverQuery
.
2. Anchored Object Query
- 설명: 데이터의 현재 스냅샷과 변경된 항목 목록 제공.
- 특징: 백그라운드 알림 등록은 불가.
- 참고:
HKAnchoredObjectQueryDescriptor
.
3. Statistics Collection Query
- 설명: 통계 계산 후 데이터 변경 시 업데이트 제공.
- 특징: 백그라운드 알림 등록은 불가.
- 참고:
HKStatisticsCollectionQueryDescriptor
.
4. Activity Summary Query
- 설명: 사용자 활동 요약 계산 후 데이터 변경 시 업데이트 제공.
- 특징: 백그라운드 알림 등록은 불가.
- 참고:
HKActivitySummaryQueryDescriptor
.
Github: Step-Tracker Repository