HealthKit Fetch Workout Time, Distance, Avg Heart Rate - swift

I was able to fetch some of the details separated from HealthKit app, but i have a missing points that i need help with such as getting the below exact data for each workout.
And here is what i tried for Exercise time:
func getExerciseTime(forDate date: Date, _ completion: ((Double, Error?) -> Void)!) {
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
let stepsCount = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.appleExerciseTime)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let interval: NSDateComponents = NSDateComponents()
interval.day = 1
let query = HKStatisticsCollectionQuery(quantityType: stepsCount!, quantitySamplePredicate: predicate, options: [.cumulativeSum], anchorDate: startDate as Date, intervalComponents: interval as DateComponents)
query.initialResultsHandler = { query, results, error in
if error != nil {
// Something went Wrong
return
}
var exercise = 0.0
if let myResults = results, let endDate = endDate {
myResults.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
if let quantity = statistics.sumQuantity() {
exercise = quantity.doubleValue(for: HKUnit.minute())
}
}
}
completion(round(exercise), error)
}
healthKitStore.execute(query)
}
I was able to get the beats but not the average heart rate for the workout:
func getAverageHeartRate(date:Date,completion:
#escaping ([HKWorkout]?, Error?) -> Void) {
guard let heartRateType =
HKObjectType.quantityType(forIdentifier:
HKQuantityTypeIdentifier.heartRate) else {
fatalError("*** Unable to create a Heart rate type ***")
}
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
// Get all workouts that only came from this app.
let workoutPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let startDateSort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: heartRateType,
predicate: workoutPredicate,
limit: 0,
sortDescriptors: [startDateSort]) { (sampleQuery, results, error) -> Void in
guard let heartRateSamples = results as? [HKQuantitySample] else {
// Perform proper error handling here.
return
}
// Use the workout's Heart rate samples here.
for heartrate in heartRateSamples {
let beats: Double? = heartrate.quantity.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute()))
print(heartrate.startDate, heartrate.endDate)
print(beats)
guard beats != nil else {
return
}
}
}
healthKitStore.execute(query)
And lastly:
func getWorkOutData(forDate date: Date, _ completion: ((Int, Error?) -> Void)!) {
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: HKQueryOptions())
let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor]) { (_, results, error ) -> Void in
var eligible = 0
if let myResults = results as? [HKWorkout] {
for workout in myResults where workout.duration >= 600 {
eligible += 1
}
}
completion(eligible, error)
}
healthKitStore.execute(sampleQuery)
}
1- I was able to get the heartbeats not the avg heart rate per workout.
2- I couldn't connect the data from each one to be like a workout model that contains Time,Distance,AvgHeart rate.

Related

HealthKit: Calculate heart rate average in a day and length of it

Getting familiar with HealthKit and i am trying to find the below by query, but haven't manage to do that yet.
During the same day workout 30 and 60 minutes with moderate heart rate (64%-76%) of max heart rate.
func getAverageHeartRate(forDate date: Date) {
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
let heartRateQuantity = HKQuantity(unit: HKUnit(from: "count/min"),
doubleValue: Double(arc4random_uniform(80) + 100))
let heartSample = HKQuantitySample(type: heartRateType,
quantity: heartRateQuantity, start: startDate, end: endDate!)
}
Try this. It has a generic routine to get any sample type. You should add error checking;
var hkSampleRecs = [HKSample]()
func getAverageHeartRate(forDate date : Date, minutes : Int, completion : #escaping (Double) -> Void)
{
let calendar = Calendar.current
let endDate = calendar.date(byAdding: .minute, value: minutes, to: startDate)
getDataSamples(dataType : "HKQuantityTypeIdentifierHeartRate", startDate : date, endDate : date, completion :
{
foundEntryCount in
var totalBPM = 0.0
for sample in self.hkSampleRecs
{
totalBPM += (sample as! HKQuantitySample).quantity.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.second()))
}
var averageBPM = (totalBPM / Double(self.hkSampleRecs.count)) * 60.0
completion(averageBPM)
}
}
func getDataSamples(dataType : HKQuantityTypeIdentifier, startDate : Date, endDate : Date, completion: #escaping (Int) -> Void)
{
var searchPredicate : NSPredicate?
searchPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let limit = 1000000
let requestedQuantityType = HKObjectType.quantityType(forIdentifier: dataType)!
let sampleQuery = HKSampleQuery(sampleType: requestedQuantityType, predicate: searchPredicate, limit: limit, sortDescriptors: [])
{
(query, result, error) in
if error != nil
{
completion(-1)
}
self.hkSampleRecs.removeAll(keepingCapacity: true)
if result != nil
{
for r in result!
{
self.hkSampleRecs.append(r)
}
}
completion(self.hkSampleRecs.count)
}
healthStore.execute(sampleQuery)
}
Call like this with 30 or 60 in the minutes field, set aDate to start time and date;
getAverageHeartRate(forDate: aDate, minutes : 30, completion:
{
average in
}

How do I display a string in View in Swift from HKSampleQuery

since this is my first Swift project and I am new to programming in general. I need some help with extracting the sample data I fetched from the query and printing it in the console. I am looking for a way to extract latestHr from the local scope and display it in the View in the app itself which is in the scope below:
var body: some View {
return VStack {
}
but I couldn't find a way to do it correctly.
This is most of the code I am using and the parts for calculation and fetching.
func latestheartRate(){
guard let sampleType = HKObjectType.quantityType(forIdentifier: .heartRate) else{return}
let startDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictEndDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: [sortDescriptor]) {(sample, result, error) in guard error == nil else {
return
}
let data = result![0] as! HKQuantitySample
let unit = HKUnit(from:"count/min")
let latestHr = data.quantity.doubleValue(for: unit)
let date = DateFormatter()
date.dateFormat = "HH:mm E, d MMM y"
let StartDate = date.string(from: data.startDate)
let EndDate = date.string(from: data.endDate)
print("Latest Hr\(latestHr) BPM // StartDate \(StartDate): EndDate \(EndDate)")
let HR = String(latestHr)
}
healthStore.execute(query)
}
//trigger the function to authorize read and write heart rate
init()
{
authorizeHealthKit()
}
var body: some View {
return VStack {
}

HKStatistics Collection Query Only returning partial Data

Below is my query. When I print out the data in my ContentView section (below), accurate data from only the past week is shown and every other day is "nil" when I know for sure there is data for those days even when the predicate should go back 30 days.
func makeElevationQuery(completion: #escaping (HKStatisticsCollection?) -> ()) {
let elevationType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.flightsClimbed)!
let calendar = NSCalendar.current
let endDate = Date()
let startDate = calendar.date(byAdding: .day, value: -30, to: endDate)
let anchorDate = Date.mondayAt12AM()
let daily = DateComponents(day : 1)
let summariesWithinRange = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
elevationQuery = HKStatisticsCollectionQuery(quantityType: elevationType, quantitySamplePredicate: summariesWithinRange, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: daily)
elevationQuery!.initialResultsHandler = {elevationQuery, statisticsCollection, error in
completion(statisticsCollection)
}
healthstore.execute(elevationQuery!)
}
}
extension Date {
static func mondayAt12AM() -> Date {
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
}
}
Content View Part:
struct ContentView: View {
#StateObject var fitness = main()
#State var final = HKActivitySummary()
#State var MonthElevation = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.flightsClimbed)
var body: some View {
Button("refresh"){
fitness.authorizeHealthkit()
fitness.makeQuery() { (summary) in
final = summary
fitness.makeElevationQuery { statisticsCollection in
if let statisticsCollection = statisticsCollection {
let startDate = Calendar.current.date(byAdding: .day, value: -30, to: Date())
let endDate = Date()
statisticsCollection.enumerateStatistics(from: startDate!, to: endDate) {
(statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
print(count) //prints out flights climbed on that day correctly for first week only but just "nil" another 23 times (because I had it go back a month)
}
}
}
}
Are you running this on watchOS or iOS? Sounds like you might be on watchOS since watch only has around a weeks worth of data. See earliestPermittedSampleDate.

Parsing results from query of HealthKit

How do you parse information received from a query? I am looking to get the active calories burned using the following:
func getActiveEnergy() {
let calendar = NSCalendar.current
let now = Date()
let components = calendar.dateComponents([.year, .month, .day], from: now)
guard let startDate = calendar.date(byAdding: .day, value: -7, to: now) else {
fatalError("*** Unable to create the start date")
}
print("startDate -> \(startDate)")
guard let endDate = calendar.date(from: components) else {
fatalError("*** Unable to create the end date")
}
print("endDate -> \(endDate)")
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
guard let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned) else {
fatalError("*** This method should never fail ***")
}
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: [sort]) { (query, results, error) in
guard let samples = results as? [HKQuantitySample] else {
print("There was an error creating the query")
return
}
for sample in samples {
print("sample from query -> \(sample)")
}
DispatchQueue.main.async {
}
}
self.healthKit.execute(query)
}
The results produce a string of information:
0.055 kcal 0FBBA46F-F9C0-4BB7-87F2-84B567900084 "Kevin’s Series 4" (6.2.1), "Watch5,2" (6.2.1)"Apple Watch" (2020-04-14 17:05:27 -0400 - 2020-04-14 17:05:37 -0400)
I only want the value (double) at the beginning of the string.
Thank you for your assistance.

Fetch All Photos from Library based on creationDate in Swift [Faster Way?]

I have a UICollectionView displaying library photos based on latest "creationDate". For that I am using below code:
struct AssetsData {
var creationDate: Date, assetResult: PHFetchResult<PHAsset>
}
func fetchPhotos() -> [AssetsData] {
//Date Formatter
let formatter = DateFormatter()
formatter.dateStyle = DateFormatter.Style.medium
formatter.timeStyle = DateFormatter.Style.none
//Photos fetch
let fetchOptions = PHFetchOptions()
let sortOrder = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.sortDescriptors = sortOrder
let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var arrCreationDate = [Date]()
var arrDates = [String]()
//Getting All dates
for index in 0..<assetsFetchResult.count {
if let creationDate = assetsFetchResult[index].creationDate {
let formattedDate = formatter.string(from: creationDate)
if !arrDates.contains(formattedDate) {
arrDates.append(formattedDate)
arrCreationDate.append(creationDate)
}
}
}
//Fetching Assets based on Dates
var arrPhotoAssetsData = [AssetsData]()
for createdDate in arrCreationDate {
if let startDate = getDate(forDay: createdDate.day, forMonth: createdDate.month, forYear: createdDate.year, forHour: 0, forMinute: 0, forSecond: 0), let endDate = getDate(forDay: createdDate.day, forMonth: createdDate.month, forYear: createdDate.year, forHour: 23, forMinute: 59, forSecond: 59) {
fetchOptions.predicate = NSPredicate(format: "creationDate > %# AND creationDate < %#", startDate as NSDate, endDate as NSDate)
let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
arrPhotoAssetsData.append(AssetsData(creationDate: createdDate, assetResult: assetsPhotoFetchResult))
}
}
return arrPhotoAssetsData
}
func getDate(forDay day: Int, forMonth month: Int, forYear year: Int, forHour hour: Int, forMinute minute: Int, forSecond second: Int) -> Date? {
var dateComponents = DateComponents()
dateComponents.day = day
dateComponents.month = month
dateComponents.year = year
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
var gregorian = Calendar(identifier: Calendar.Identifier.gregorian)
gregorian.timeZone = NSTimeZone.system
return gregorian.date(from: dateComponents)
}
The code works nicely! But the problem is it takes almost 7 - 9 seconds to load 10k+ photos. Till 6k photos there is no problem, but I really need some efficient way so that I can load some of the asset in UICollectionView and rest of them I can add later. I need that no matter the photos count, it should not take more than 2 - 3 seconds. Can anybody please help?
Let's say you have 8k photos. So you iterate through two 'for' loops in order to get the arrCreationDate and arrPhotoAssets data(which is double the work needed)
Instead, you can try doing it through a single loop. Here's a rough way:-
let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let fetchOptions = PHFetchOptions()
var arrCreationDate = [Date]()
var arrPhotoAssetsData = [AssetsData]()
var arrDates = [String]()
for index in 0..<assetsFetchResult.count {
if let creationDate = assetsFetchResult[index].creationDate {
let formattedDate = formatter.string(from: creationDate)
if !arrDates.contains(formattedDate) {
//You can convert the formattedDate to actual date here and do a check similar to this, do what you do in the other loop here too
if(actualDate < actualDateOfTheFirstElementAtArray){
arrCreationDate.insert(actualDate, at: 0)
fetchOptions.predicate = NSPredicate(format: "creationDate > %# AND creationDate < %#", startDate as NSDate, endDate as NSDate)
let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
arrPhotoAssetsData.insert(AssetsData(creationDate: createdDate, assetResult: assetsPhotoFetchResult), at: 0)
}
}
}
}
This is just for you to get a rough idea of what I'm talking about, as this will reduce half the burden(just a single loop)
Also try using prefetchDataSource for your collection view to preload it with some data
EDIT:-
I assume that you have tried the following already:-
func fetchPhotos() -> [AssetsData] {
//Date Formatter
let formatter = DateFormatter()
formatter.dateStyle = DateFormatter.Style.medium
formatter.timeStyle = DateFormatter.Style.none
//Photos fetch
let fetchOptions = PHFetchOptions()
let sortOrder = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.sortDescriptors = sortOrder
let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var arrCreationDate = [Date]()
var arrDates = [String]()
var arrPhotoAssetsData = [AssetsData]()
//Getting All dates
for index in 0..<assetsFetchResult.count {
if let creationDate = assetsFetchResult[index].creationDate {
let formattedDate = formatter.string(from: creationDate)
if !arrDates.contains(formattedDate) {
arrDates.append(formattedDate)
arrCreationDate.append(creationDate)
convertToAssetsDataAndAppend(date: creationDate, fetchOptions: fetchOptions, toArray: &arrPhotoAssetsData)
}
}
}
return arrPhotoAssetsData
}
func convertToAssetsDataAndAppend(date: Date, fetchOptions: PHFetchOptions, toArray: inout [AssetsData]){
if let startDate = getDate(forDay: date.day, forMonth: date.month, forYear: date.year, forHour: 0, forMinute: 0, forSecond: 0), let endDate = getDate(forDay: date.day, forMonth: date.month, forYear: date.year, forHour: 23, forMinute: 59, forSecond: 59) {
fetchOptions.predicate = NSPredicate(format: "creationDate > %# AND creationDate < %#", startDate as NSDate, endDate as NSDate)
let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
toArray.append(AssetsData(creationDate: date, assetResult: assetsPhotoFetchResult))
}
}
func getDate(forDay day: Int, forMonth month: Int, forYear year: Int, forHour hour: Int, forMinute minute: Int, forSecond second: Int) -> Date? {
var dateComponents = DateComponents()
dateComponents.day = day
dateComponents.month = month
dateComponents.year = year
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
var gregorian = Calendar(identifier: Calendar.Identifier.gregorian)
gregorian.timeZone = NSTimeZone.system
return gregorian.date(from: dateComponents)
}
If this doesn't help, how about reloading the collection view with some kind of callback after every loop iteration? (with the above approach)
This way, you won't make the user wait until the whole thing gets loaded
Idk, these might look petty but I'm just trying to help :)