티스토리 뷰

WWDC

[WWDC20] Getting started with HealthKit 정리

Hani_Levenshtein 2021. 11. 1. 00:34

안녕하세요 Hani입니다.

이번에는 WWDC20에서 발표된 Getting started with HealthKit에 대하여 정리해볼 거예요. 🥰

 

 

쫘란

오늘의 주인공 HealthKit ☺️

 

HealthKit은 사용자의 Health Data에 대한 중앙 저장소를 생성하는 프레임워크로,

HealthKit 덕분에 애플리케이션이 Health Data을 통해 데이터를 읽고 쓸 수 있습니다. 

 

iPhone과 Apple Watch, iCloud 등 여러 장치에서 Health Data와 상호작용 할 수 있어요. 

HealthKit이 Health Data를 안전하게 보호하고 동기화해주기 때문에 개발자가 이에 대한 처리를 할 필요가 없습니다. ☺️

 

일단 HealthKit을 사용하려면 몇 가지 작업해야 할 것들이 있는데

엑스코드에서 HealthKit을 활성화하고

플랫폼이 HealthKit을 지원하는지 확인하고

HealthKit API의 Entry Point인 HKHealthStore 클래스의 인스턴스를 생성해야 합니다.

 

프로젝트에서 Capability를 누르고 HealthKit을 검색하여 추가한 다음에

 

import HealthKit

var healthStore: HKHealthStore?

//HealthKit이 플랫폼에서 사용가능한지 확인
if HKHealthStore.isHealthDataAvailable() {

	//쌉가능
	healthStore = HKHealthStore()

} else {
	//안됨ㅠ
}

플랫폼이 지원하는지 확인하고 인스턴스를 생성하면 세팅이 완료됩니다. ☺️

 

( iPad같은 플랫폼은 HealthKit을 지원하지 않아요. 🥺 )

 

 

여기서 HKHealthStore란

HealthKit이 관리하는 Health Data에 접근하기 위한 객체로,

데이터를 저장할 수도 있고, 쿼리를 통해 데이터를 읽어올 수도 있습니다. ☺️

 

이를 위해선 사용자에게 권한을 요청해야 해요. 🥺

 

앱의 생명주기에 걸쳐 재사용하면 되므로 앱 당 하나의 인스턴스만 생성하면 끝 🦀

 

 

HealthKit을 셋업하는 방법과 HKHealthStore에 대하여 알아봤으니까

HealthKit이 어떻게 Health Data를 구성하는지 알아봅시다. 

HealthKit은 HKSample이라는 클래스의 서브클래스를 이용하여 대부분의 데이터를 저장합니다.

 

먼저, 값과 단위를 포함한 정량화된 데이터를 보여주는 HKQuantitySample를 살펴보면,

HKQuantitySample은 주어진 단위에 대한 값을 저장하는 객체인 HKQuantity를 하나 이상 포함하고 있는 객체입니다.

 

 

오호

 

 

HealthKit이 어떻게 Health Data를 구성하는지 알았으니 

권한을 요청하는 방법을 알아볼게요. ☺️

애플리케이션에서 유저의 Health Data에 접근하기 위해서는 권한을 받아야 해요. ☺️

 

여기서 권한은 원하는 정보에 대한 타입별로 승인받아야 하고

읽기와 쓰기 작업에 대하여 따로따로 받아야 합니다. 

 

유저가 모든 권한을 주지 않을 수도 있어요 🥺

 

 

따라서 유저에게 권한을 허락해달라고 아부를 잘 떨어야 하는데

애플리케이션이 왜 정보를 요구하는 지 알려줘야 합니다.

 

이런 식으로 Info.plist에 추가하고 코멘트를 작성해주면 끝 ☺️

 

 

또한 Health Data를 필요로하는 시점에 요구를 해야 합니다.

수영 중인데 수영과 관련없는 정보를 달라고 하면 엥? 하겠죠? 😤

 

 

그리고 사용자 권한은 애플리케이션 외부에서 언제든 변경될 수도 있으니까 

해당 정보가 필요한 시점에 항상 요청을 해야 합니다.

 

 

정리하자면

필요한 타이밍에 원하는 권한만을 항상 요청하자 😎

 

 

 

권한에 대하여 알아봤으니까 

쓰기 작업을 한 번 해볼까용

운동한 거리를 저장하려면

해당 타입에 대한 쓰기에 대한 권한을 얻어야 하고,

운동 거리에 대한 샘플을 만들고

HealthStore를 사용하여 HealthKit에 샘플을 저장해야 합니다. 

 

 

//쓰기 권한 요청
let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!

healthStore?.requestAuthorization(toShare: [distanceType], read: nil) { success, error in
    if success {
        
    } else {
        
    }
}

distanceWalkingRunning 타입에 대한 권한을 요청하고

 

let startDate = Calendar.current.date(bySettingHour: 14, minute: 35, second: 0, of: Date())!
let endDate = Calendar.current.date(bySettingHour: 15, minute: 0, second: 0, of: Date())!

let distanceQuantity = HKQuantity(unit: HKUnit.meter(), doubleValue: 628.0)

//샘플 생성
let sample = HKQuantitySample(type: distanceType,
                              quantity: distanceQuantity,
                              start: startDate,
                              end: endDate)

//샘플 저장
healthStore?.save(sample) { success, error in
    if success {
        
    } else {
        
    }
    
}

단위와 값을 갖는 HKQuantity 객체를 생성하여

객체를 이용하여 HKQuantitySample을 만들고

HealthStore를 통해 샘플을 저장 🥰 

 

 

하지만 뭔가 정량적으로 나타내기 어려운 데이터들은 어떻게 할까요? 🥺

 

HealthKit은 HKQuantitySample 뿐만 아니라 다양한 Sample을 제공하고 있습니다. ☺️

이런 Sample 덕에 HealthKit이 다루는 Health Data의 종류는 백개가 넘는다고 합니다.

 

사실 모든 Sample은 HKSample이라는 추상 클래스로부터 구체화된 서브클래스들이에요. 🥰

 

생일이나 혈액형처럼 시간이 흘러도 변하지 않는 값들은 HKCharacteristic에 저장됩니다.

HKObject도 추상 클래스 🦀

 

그리고 이 모든 계층에 대하여 대응되는 타입이 존재하는데

 

이 타입들은

해당 타입에 대한 읽기/쓰기 권한을 얻거나

샘플을 생성하여 쓰거나

쿼리를 보내서 읽을 때

사용합니다. 

 

 

 

이제 마지막 관문인 읽기 작업에 대하여 알아봅시당.

읽기는 쿼리를 통해서 이뤄지는데

쿼리는 HealthKit으로부터 데이터를 받기 위한 객체입니다.

 

역시 HKQuery라는 추상클래스를 서브클래싱한 여러 쿼리들이 존재하는데

여기서는 몇 개만 다뤄볼 거예욥. 🥰

 

쿼리를 이용하는 방법은

쿼리를 만들고

HealthStore를 통해 쿼리를 실행하고

핸들러로부터 결과를 전달받습니다.

 

ppt에 오타가 있군요 ☺️ (소곤소곤)

 

우리가 먼저 알아볼 쿼리는 HKStatisticsQuery입니다.

 

결과가 축적되는 것이 의미가 있는 데이터도 있고 ( 운동 중 체온의 합계 같은 데이터는 알려주지 않아요.. 😤 )

평균값, 최소/최댓값이 의미가 있는 데이터도 있죠?

 

 

여기서는 걸음 데이터를 쿼리로 써봅시다 ☺️

6월 15일부터 Today까지의 기록을 HKStatisticsQuery로 요청하면

총걸음의 합계를 얻어낼 수 있습니다. ☺️

 

반환 값은 HKStatistics 객체군요.

 

만약 애플 워치랑 아이폰을 둘 다 소지한 채로 달렸다?

중복된 기록은 삭제하고 알아서 똑똑하게 합쳐준다 이 말씀 ☺️

 

6월 15일부터 Today까지의 기록을 합쳐서 보지 않고

하루마다 따로 보고 싶다면 HKStatisticsQuery를 여러 개 요청해야 합니다. 🥺

 

그건 쫌!

 

 

그래서 준비한 HKStatisticsCollectionQuery 🥰

한 번만 요청하면 Time Interval에 따라 나눠진 HKStatistics들이 쫙 나옴~!

 

HKStatisticsCollection이라는 HKStatistics 배열을 담을 수 있는 객체에 데이터들이 쌓여 반환됩니다.

 

HKStatisticsCollectionQuery를 통해 얻은 데이터로 차트를 그리면 딱 ☺️

 

 

일단 HKStatisticsCollectionQuery로 데이터 받는 것부터 해봅시다.

HKStatisticsCollectionQuery는 AnchorDate와 TimeInterval을 이용하여

고정된 기간 안의 여러 HKStatistics를 얻을 수도 있지만

updateHandler를 통해 Health Data의 업데이트된 내용을 수신할 수도 있어요. ☺️ 

 

 

이를 위해선 쿼리를 execute 하기 전에 updateHandler를 설정해줘야 합니다.

 

 

 

데이터를 보여주기 위해선

쿼리를 구성하고

실행한 다음

결과를 UI에 반영하기만 하면 끝 ☺️

 

 

 

다음은 코드를 보여드릴 건데 동영상 보고 정말 대충 짠 HKStatisticsCollectionQuery 코드예요..

단지 이렇게 테이블뷰 만들면 데이터 볼 수 있다 그런 느낌..

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!

        healthStore.requestAuthorization(toShare: [stepType], read: [stepType]) { success, _ in
            if success {
                self.calculatetDailyStepCountForPastWeek()
            } else {

            }
        }
    }

권한 요청하기 적절한 곳은 viewWillAppear이기 때문에

여기서 걸음 수에 대한 타입(HKQuantityType)을 만들고

healthStore를 통해 권한을 요청합니다.

 

func calculatetDailyStepCountForPastWeek() {
        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        let monday = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        let daily = DateComponents(day: 1)
        let exactlySevenDaysAgo = Calendar.current.date(byAdding: DateComponents(day: -7), to: Date())!
        let oneWeekAgo = HKQuery.predicateForSamples(withStart: exactlySevenDaysAgo, end: nil, options: .strictStartDate)

        self.query = HKStatisticsCollectionQuery(quantityType: stepType,
                                                 quantitySamplePredicate: oneWeekAgo,
                                                 options: .cumulativeSum,
                                                 anchorDate: monday,
                                                 intervalComponents: daily)

        self.query?.initialResultsHandler = { _, statisticsCollection, _ in
            if let statisticsCollection = statisticsCollection {
                self.updateUIFromStatistics(statisticsCollection)
            }
        }

        self.query?.statisticsUpdateHandler = { _, _, statisticsCollection, _ in
            if let statisticsCollection = statisticsCollection {
                self.updateUIFromStatistics(statisticsCollection)
            }

        }

        self.healthStore.execute(query!)
    }

권한을 성공적으로 받은 후에는

HKStatisticsCollectionQuery에 필요한 전달인자를 여기저기 구해와서 쿼리를 만듭니다. 

 

 

그다음은 Handler를 설정할 수 있는데

 

initialResultsHandler는 쿼리의 초기 결과를 다루는 핸들러기 때문에 꼭 설정해줘야 해요. 🥺

HKStatisticsCollectionQuery initialResultsHandler cannot be nil

 

 

statisticsUpdateHandler는 Observer 쿼리처럼 작동하여

Health Data의 업데이트 사항이 생기면 작동합니다. 

필수는 아님!

 

 

마지막으로는 healthStore를 통해 쿼리를 execute.

 

 

 func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
        DispatchQueue.main.async {
            self.dateValues = []

            let startDate = Calendar.current.date(byAdding: .day, value: -6, to: Date())!
            let endDate = Date()

            statisticsCollection.enumerateStatistics(from: startDate, to: endDate) { [weak self] (statistics, _) in
                self?.dateValues.append(statistics)
            }
            self.tableView.reloadData()
        }
    }

HKStatisticsCollectionQuery를 통해 얻어진 Statistics들을 UI에 반영할 땐

eumerateStatistics로 HKStatistic들을 순회할 수 있습니다.

 

메인 스레드에서 실행해줘야겠죠? 😎

 

 

 override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.healthStore.stop(query!)
    }

더 이상 데이터가 필요하지 않으면(viewWillDisappear)

healthStore를 통해 쿼리를 중지하면 됩니다.

 

 

 

 

종합 코드

final class ViewController: UIViewController {

    @IBOutlet private weak var tableView: UITableView!

    var healthStore =  HKHealthStore()
    var query: HKStatisticsCollectionQuery?
    var dateValues: [HKStatistics] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.dataSource = self
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!

        healthStore.requestAuthorization(toShare: [stepType], read: [stepType]) { success, _ in
            if success {
                self.calculatetDailyStepCountForPastWeek()
            } else {

            }
        }
    }

    func calculatetDailyStepCountForPastWeek() {
        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        let monday = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        let daily = DateComponents(day: 1)
        let exactlySevenDaysAgo = Calendar.current.date(byAdding: DateComponents(day: -7), to: Date())!
        let oneWeekAgo = HKQuery.predicateForSamples(withStart: exactlySevenDaysAgo, end: nil, options: .strictStartDate)

        self.query = HKStatisticsCollectionQuery(quantityType: stepType,
                                                 quantitySamplePredicate: oneWeekAgo,
                                                 options: .cumulativeSum,
                                                 anchorDate: monday,
                                                 intervalComponents: daily)

        self.query?.initialResultsHandler = { _, statisticsCollection, _ in
            if let statisticsCollection = statisticsCollection {
                self.updateUIFromStatistics(statisticsCollection)
            }
        }

        self.query?.statisticsUpdateHandler = { _, _, statisticsCollection, _ in
            if let statisticsCollection = statisticsCollection {
                self.updateUIFromStatistics(statisticsCollection)
            }

        }

        self.healthStore.execute(query!)
    }

    func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
        DispatchQueue.main.async {
            self.dateValues = []

            let startDate = Calendar.current.date(byAdding: .day, value: -6, to: Date())!
            let endDate = Date()

            statisticsCollection.enumerateStatistics(from: startDate, to: endDate) { [weak self] (statistics, _) in
                self?.dateValues.append(statistics)
            }
            self.tableView.reloadData()
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.healthStore.stop(query!)
    }
}

extension ViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        self.dateValues.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as? HomeTableViewCell else { return UITableViewCell() }
        cell.configure(with: dateValues[indexPath.row])
        return cell
    }

}

 

 

 

여기서 모든 쿼리를 다루진 못하지만 참고하면 좋을 쿼리들 🥰

 


References

https://developer.apple.com/videos/play/wwdc2020/10664/

 

댓글