1. 애플워치 추가하기

- File - New - Target 로 애플워치 앱 target를 추가해줍니다.

 

선택사항이 있는데 종류의 의미는 다음과 같습니다.

 

Watch-only App

- 이 옵션은 Apple Watch 에서만 실행되는 독립적인 watchOS 앱을 생성합니다. iOS 앱이 없어도 동작합니다

 

Watch App with New Companion iOS App

- 이 옵션은 watchOS 앱과 함께 새로운 iOS 앱을 동시에 생성하여 동반 앱으로 설정합니다.

 

Watch App for Existing iOS App (기존 iOS 앱을 위한 워치 앱)

- 이 옵션은 현재 존재하는 iOS 앱 프로젝트에 watchOS 앱 타깃을 추가하여 동반 앱으로 설정합니다.

 

 

 

 

 

2. Bundle Identifier, Info.plist 설정하기

XCode에서 애플워치를 사용하기 위해서는 Bundle Indentifier 를 수정해야합니다. WatchKit 앱의 번들 ID는 생성된 앱의 Bundle Identifier에 .watchkitapp 을 추가한 형태여야 합니다. (다음과 같이 말이죠)

 

다음으로는 Info.plist 를 설정해야 합니다.

WatchKit 앱 대상의 Info.plist 파일을 열고 WKCompanionAppBundleIdentifier 키의 값을 새 iOS 앱 번들 식별자로 업데이트합니다. 이것은 WatchKit 앱과 연동되는 iOS 앱의 Bundle Identifier를 설정하는 것 입니다.

 

추가로 NSHealthUpdateUsageDescription 를 설정하여 사용자한테 심박수를 측정하기 위한 요청에 대한 설명을 작성합니다.

추가로 NSHealthShareUsageDescription 를 설정하여 사용자한테 건강앱과 데이터를 공유하는 것에 대한 설명을 작성합니다.

 

마지막으로 Capability 에서 HealthKit 을 추가해야 합니다.

3. 권한 받기

필요한 권한을 다음과 같이 설정합니다. 현재 심박수를 가져오기 때문에 .heartRate를 통해서 심박수 권한을 요청합니다.

코드 설명

HKQuantityType(.heartRate) 를 사용하여 숫자 값인 샘플 타입을 나타냅니다.

func requestAuthorization(toShare: Set<HKSampleType>?, read: Set<HKObjectType>?, completion: (Bool, (any Error)?) -> Void)를 사용하여 toShare로 지정한 타입의 데이터를 읽거나 쓸 수 있도록 권한을 요청합니다.

func autorizeHealthKit() {
    let healthKitTypes: Set = [HKQuantityType(.heartRate)]
    healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { _, _ in }
}

 

4. 심박수 쿼리 시작

HealthKit 프레임워크를 사용하여 심박수 데이터를 조회를 시작합니다.

코드 설명

HKQuery.predicateForObjects(from:) 메서드를 통해서 현재 데이터를 가져올 기기를 사용자 아이폰으로 설정합니다.

updateHandler 객체를 만들어 측정값을 samples에 담아내고 process 함수를 실행합니다.

HKAnchoredObjectQuery(type:, predicate, anchor, limit, resultsHandler:)메서드로 수행할 쿼리를 만들고 핸들러를 추가합니다.

healthStore.execute(HKQuery)로 제공된 쿼리를 시작하며 계속해서 결과를 Handler를 통해서 받아 작업을 수행합니다.

.execute는 healthStore.stop(HKQuery)로 중단할 수 있습니다
private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
    let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
    let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
        query, samples, deletedObjects, queryAnchor, error in

        guard let samples = samples as? [HKQuantitySample] else {
            return
        }
        self.process(samples, type: quantityTypeIdentifier)
    }

    let query = HKAnchoredObjectQuery(type: HKQuantityType(quantityTypeIdentifier), predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler)
    query.updateHandler = updateHandler
    healthStore.execute(query)
}

 

5. 워치로부터 받은 심박수 다루기

애플워치로부터 심박수 데이터를 측정하여 healthStore로 수행한 쿼리의 핸들러로 받은 심박수를 처리할 수 있습니다.

예제 코드

심박수는 다음과 같이 samples에 저장되어 있으며 만약 .heartRate 타입이라면 저장 후 UI에 보여줍니다.

private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
    var lastHeartRate = 0.0
    for sample in samples {
        if type == .heartRate {
            lastHeartRate = sample.quantity.doubleValue(for: heartRateQuantity)
        }
        self.value = Int(lastHeartRate)
    }
}

 

 

전체 코드

import SwiftUI
import HealthKit

struct test: View {
    private var healthStore = HKHealthStore()
    let heartRateQuantity = HKUnit(from: "count/min")
    
    @State private var value = 0
    
    var body: some View {
        VStack{
            HStack{
                Text("❤️")
                    .font(.system(size: 50))
                Spacer()
                
            }
            
            HStack{
                Text("\(value)")
                    .fontWeight(.regular)
                    .font(.system(size: 70))
                
                Text("BPM")
                    .font(.headline)
                    .fontWeight(.bold)
                    .foregroundColor(Color.red)
                    .padding(.bottom, 28.0)
                
                Spacer()
                
            }
            
        }
        .padding()
        .onAppear(perform: start)
    }
    
    
    func start() {
        autorizeHealthKit()
        startHeartRateQuery(quantityTypeIdentifier: .heartRate)
    }
    
    func autorizeHealthKit() {
        let healthKitTypes: Set = [HKQuantityType(.heartRate)]
        healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { _, _ in }
    }
    
    private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
        let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
        let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
            query, samples, deletedObjects, queryAnchor, error in
            
            guard let samples = samples as? [HKQuantitySample] else {
                return
            }
            self.process(samples, type: quantityTypeIdentifier)
        }
        
        let query = HKAnchoredObjectQuery(type: HKQuantityType(quantityTypeIdentifier), predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler)
        query.updateHandler = updateHandler
        healthStore.execute(query)
    }
    
    private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
        var lastHeartRate = 0.0
        for sample in samples {
            if type == .heartRate {
                lastHeartRate = sample.quantity.doubleValue(for: heartRateQuantity)
            }
            
            self.value = Int(lastHeartRate)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        test()
    }
}
ytw_developer