[SwiftUI] FireStore에 HealthKit 데이터를 저장하는 방법

HealthKit 가져오기에 대해서는 다음 기사를 참고하도록 하겠습니다.
https://zenn.dev/ueshun/articles/dd700cdbb61f8d
개인적으로 HealthStore 데이터를 처리할 때 데이터를 다른 데이터베이스에 저장할 필요가 없습니다.
다른 사람과 공유하려면 그게 필요해.
이번에는 헬스키트의 데이터를 Firestore에 저장하는 방법을 간단하게 요약해 보겠습니다.
저장된 데이터의 종류는 어제까지의 걸음수를 매일 합한 것이다.
  • 권한 요청
  • Anchor 사용 중 지정
  • HKcollectionQuery의 데이터를 사용하여 취득, 보존
  • 실행
  • 권한 요청


    https://developer.apple.com/documentation/healthkit/hkhealthstore/1614152-requestauthorization
    기능의 확장성을 시야에 두다
  • 어떤 것을 처리할 것인가HKSampleType
  • 어떤 HKSampleType에 대한 권한을 부여하는가
  • HKSampleType을 얻기 위해 처리된 데이터의 종류를 정리합니다


    var allHealthDataTypes: [HKSampleType] {
    let typeIdentifiers: [String] = [
    
        HKQuantityTypeIdentifier.stepCount.rawValue
    
    	]
    
    	return typeIdentifiers.compactMap {
    	    getSampleType(for: $0)
    	}
    }
    
    func getSampleType(for identifier: String) -> HKSampleType? {
    
    	if let quantiryType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier(rawValue: identifier )) {
    	    return quantiryType
    	}
    
    	if let categoryType = HKCategoryType.categoryType(forIdentifier: HKCategoryTypeIdentifier(rawValue: identifier)) {
    	    return categoryType
    	}
    
    	return nil
    
    }
    
    이번 처리는 歩数typeIdentifiers보존하다.HKQuantityTypeIdentifier.stepCount.rawValue에서 typeIdentifiers에서 취득getSapmleType.

    처리 데이터에 대한 쓰기 권한과 읽기 권한을 요청하는 함수를 준비합니다


    func requestHealthDataAccessIfNeeded(dataTypes: [String]? = nil, completion: @escaping (_ success: Bool) -> Void) {
    	var readDataTypes = Set(allHealthDataTypes)
    	var shareDataTypes = Set(allHealthDataTypes)
    
    	if let dataTypeIdentifiers = dataTypes {
    	    readDataTypes = Set(dataTypeIdentifiers.compactMap { getSampleType(for: $0) })
    	    shareDataTypes = readDataTypes
    	}
    
    	requestHealthDataAccessIfNeeded(toShare: shareDataTypes, read: readDataTypes, completion: completion)
    }
    
    
    func requestHealthDataAccessIfNeeded(toShare shareTypes: Set<HKSampleType>?, read readTypes: Set<HKObjectType>?, completion: @escaping (_ success: Bool) -> Void) {
    	if !HKHealthStore.isHealthDataAvailable() {
    	    fatalError("Health data is not available!")
    	}
    
    	print("Requesting HealthKit authorization...")
    	healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { (success, error) in
    	    if let error = error {
    		print("requestAuthorization error:", error.localizedDescription)
    	    }
    
    	    if success {
    		print("HealthKit authorization request was successful!")
    	    } else {
    		print("HealthKit authorization was not successful.")
    	    }
    
    	    completion(success)
    	}
    }
    
    HKSampleType는 해당 데이터의 쓰기 권한과 읽기 권한을 요청하는 함수를 준비합니다.allDataTypes에는 쓰기 데이터의 종류가 대입되고, toShare에는 읽기 데이터의 종류가 대입된다.
    (이번에는 읽기만 했지만 향후 확장성을 고려해 쓰기 권한도 부여함)

    HKCollectionQuery를 사용하여 데이터 가져오기, 저장


    언제부터 언제까지의 데이터를 얻다

    read를 사용하여 데이터를 취득한 경우HKCollectionQuery 질의를 지정하고 실행합니다.
    예를 들어 2021년 9월 13일 오전 0시부터 2021년 9월 20일 오전 0시까지의 데이터를 얻으려면 다음과 같다.
    let startDate = DateComponents(year: 2021, month: 9, day: 13, hour: 0, minute: 0, second: 0)
    let endDate = DateComponents(year: 2021, month: 9, day: 20, hour: 0, minute: 0, second: 0)
    
    let predicate = HKQuery.predicateForSamples(
        withStart: calendar.date(from: startDate),
        end: calendar.date(from: endDate)
    )
    
    이번의 경우 처음 이후predicate에 저장된 데이터가 중복되지 않도록 지난번에 저장한 데이터와 차이가 있어야 한다.
    Firestore에 언제까지 데이터 기록으로 저장해야 하는가.
    이를 위해 준비한다Firestore.

    anchor 함수 저장 및 가져오기

    anchor는 마지막으로 언제까지 데이터를 입수했음을 나타내는 anchor형 Object다.
    Firestore에 저장하면 다음Date 실행 시querystartDate로 받을 수 있습니다.
    class AnchorEntity: Codable, Identifiable {
        var id: String
        var createdAt: Date
    
        init(id: String, createdAt: Date) {
            self.id = id
            self.createdAt = createdAt
        }
    }
    
    func saveAnchor(from date: Date) -> Future<Void, Error> {
    
    return Future<Void, Error> { promise in
    
    	    let anchorId = IdFactory.create()
    
    	    let anchorParams: [String: Any] = [
    		"id": anchorId,
    		"createdAt": Timestamp(date: date)
    	    ]
    
    	    docRef.collection("anchor")
    		  .document(anchorId)
    		  .setData(anchorParams)
    
    	    promise(.success(Void()))
    
    	}
    }
    
    anchor는 소장에 문서로 순서대로 추가했다.
    func getAnchor(from date: Date) -> Future<Date, Error> {
    	return Future<Date, Error> { promise in
    
    	    let docsRef = 
    		docRef
    		.collection("anchor")
    		.order(by: "createdAt", descending: true).limit(to: 1)
    
    	    docsRef.getDocuments { snapshot, error in
    		if let error = error {
    		    print(error)
    		    return
    		}
    
    		guard let snapshot = snapshot else {
    		    print("Error fetching snapshot: \(error!)")
    
    		    return
    		}
    
    		let anchor = try? snapshot.documents.first?.data(as: AnchorEntity.self)
    
    		if let anchor = anchor {
    		    promise(.success(anchor.createdAt))
    
    		} else {
    		    promise(.success(Calendar.current.date(byAdding: .day, value: -7, to: date)!))
    
    		}
    	    }
    	}
    }
    
    anchor 소장품에서 최신을 꺼냅니다.anchor 취득할 수 있는 데이터(처음 저장할 때)가 없으면 7일 전의 날짜를 anchor로 반환한다.

    HKCollectionQuery를 실행할 함수 준비


    func fetchStatistics(with identifier: HKQuantityTypeIdentifier,
    		 predicate: NSPredicate? = nil,
    		 options: HKStatisticsOptions,
    		 startDate: Date,
    		 endDate: Date = Date(),
    		 interval: DateComponents,
    		 completion: @escaping (HKStatisticsCollection?, Error?) -> Void) {
    guard let quantityType = HKObjectType.quantityType(forIdentifier: identifier) else {
        fatalError("*** Unable to create a step count type ***")
    }
    
    let anchorDate = createAnchorDate()
    
    let query = HKStatisticsCollectionQuery(quantityType: quantityType,
    					quantitySamplePredicate: predicate,
    					options: options,
    					anchorDate: anchorDate,
    					intervalComponents: interval)
    
    query.initialResultsHandler = { query, results, error in
        completion(results, error)
    }
    
    healthStore.execute(query)
    }
    
    func createAnchorDate() -> Date {
    let calender: Calendar = .current
    var anchorComponents = calender.dateComponents([.day, .month, .year, .weekday], from: Date())
    let offset = (7 + (anchorComponents.weekday ?? 0) - 2) % 7
    
    anchorComponents.day! -= offset
    anchorComponents.hour = 0
    
    let anchorDate = calender.date(from: anchorComponents)!
    
    return anchorDate
    }
    
    는 사전에 준비anchor를 하면 집행하기 쉽다fetchStatistics.이 경우HKStatisticsCollectionQuery에서 제작된createAnchorDate은 HK Statics CollectionQuery를 실행하기 위한 것으로 이전anchorDate과는 무관하다.
    https://developer.apple.com/documentation/healthkit/hkstatisticscollectionquery

    데이터 가져오기 및 저장 함수 준비


    데이터가 FireStore에 anchor로 저장됨
    class HealthQuantityEntity: Codable, Identifiable {
    
        enum HealthQuantityType: String, Codable {
            case step
        }
    
        var id: String
    
        var startDate: Date
    
        var endDate: Date
    
        var data: Double
    
        var dataType: HealthQuantityType
    
        var unit: String
    
        init(id: String,
             startDate: Date,
             endDate: Date,
             data: Double,
             dataType: HealthQuantityType,
             unit: String) {
    
            self.id = id
            self.startDate = startDate
            self.endDate = endDate
            self.data = data
            self.dataType = dataType
            self.unit = unit
        }
        
    }
    
    func saveSteps(startDate: Date, endDate: Date) -> Future<Void, Error> {
    
    	return Future<Void, Error> { promise in
    
    	    let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
    	    let dateInterval = DateComponents(day: 1)
    	    let statisticsOptions = HKStatisticsOptions.cumulativeSum
    
    	    let initialResultsHundler: (HKStatisticsCollection?, Error?) -> Void = { (statisticsCollection, error) in
    
    		if let error = error {
    		    promise(.failure(error))
    		    return
    		}
    
    		statisticsCollection?.enumerateStatistics(from: startDate, to: endDate) { (statistics, stop) in
    
    		    let stepsId = IdFactory.create()
    
    		    var stepsParams: [String: Any] = [
    
    			"id": stepsId,
    			"startDate": Timestamp(date: statistics.startDate),
    			"endDate": Timestamp(date: statistics.endDate),
    			"data": 0,
    			"dataType": HealthQuantityEntity.HealthQuantityType.step.rawValue,
    			"unit": "歩"
    		    ]
    
    		    let statisticsQuantity = statistics.sumQuantity()
    
    		       let value = statisticsQuantity?.doubleValue(for: .count) {
    			stepsParams["data"] = value
    
    			docRef
    			    .collection("HealthQuantity")
    			    .document(stepsId)
    			    .setData(stepsParams)
    
    		    } else {
    			promise(.failure(Errors.invalid))
    			return
    		    }
    		}
    
    	    }
    
    	    healthKitSerivice.fetchStatistics(with: HKQuantityTypeIdentifier.stepCount,
    					      predicate: predicate,
    					      options: statisticsOptions,
    					      startDate: startDate,
    					      interval: dateInterval,
    					      completion: initialResultsHundler)
    
    	    promise(.success(Void()))
    	}
    
    }
    
    HealthQuantityEntity 실행 후 되돌아오는 데이터는 fetchStatistics 내의 initialResultsHundler를 통해 매일 합계 계산 결과를 계산한다.

    실행


    위 함수를 호출하면FireStore에서 어제까지 반복하지 않고 저장할 수 있는 하루 합계입니다.
    requestHealthDataAccessIfNeeded(dataTypes: [HKQuantityTypeIdentifier.stepCount.rawValue]) { (success) in
        if success {
    
    	let now = Date()
    	let calender: Calendar = .current
    
    	var endDateComponents = calender.dateComponents([.day, .month, .year, .weekday], from: now)
    	endDateComponents.hour = 0
    	let endDate = calender.date(from: endDateComponents)!
    
    	self.getAnchor(from: endDate)
    
        } 
    }
    
    func getAnchor(from date: Date) {
    
    	getAnchor(from: date).sink { err in
    
    	    print(err)
    
    	} receiveValue: { startDate in
    
    	    if startDate != date {
    
    		self.saveSteps(startDate: startDate, endDate: date)
    		self.saveAnchor(from: date)
    
    	    }
    
    	}.store(in: &cancellables)
    
    }
    
    취득 후enumerateStatistics 보존 단계는 anchor에 따라 최신saveSteps을 보존한다.
    func saveSteps(startDate: Date, endDate: Date) {
    
    	saveSteps(startDate: startDate, endDate: endDate).sink { err in
    	    print(err)
    	} receiveValue: { _ in
    
    	}.store(in: &cancellables)
    
    }
    
    func saveAnchor(from date: Date) {
    
    	saveAnchor(from: date).sink { err in
    	    print(err)
    	} receiveValue: { _ in
    
    	}.store(in: &cancellables)
    
    }
    

    좋은 웹페이지 즐겨찾기