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

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
}

Related

HealthKit Fetch Workout Time, Distance, Avg Heart Rate

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.

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.

operation with hours using swift, sum and difference

On my project I need to work with hours and min.
I can't find to much info online.
here my issue:
let time1 = "22:00"
let time2 = "20:00"
Question 1:
how do I subtract the time1 - time2 ?
I'm expecting result = 02:00 h
i start writing some code, converting this time to date..
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
var time1d = formatter.date(from: time1)!
let time2d = formatter.date(from: time2)!
but now how do I subtract this two hours?
Question 2:
let time1 = "22:00"
let time2 = "20:00"
How do i sum time1 + time2 ? for example should give me result 42:00 hours
thanks for the help
This is not a Date. If you are only gonna work with hours and minutes and the input will always be a string properly formatted you should struct your data.
Create a Time structure
struct Time {
let hour: Int
let minute: Int
}
And a custom initializer. This assumes your string is always properly formatted 00:00:
extension Time {
init?(string: String) {
guard string.count == 5,
Array(string)[2] == ":",
let hour = Int(string.prefix(2)),
let minute = Int(string.suffix(2)),
0...59 ~= minute else {
return nil
}
self.hour = hour
self.minute = minute
}
}
For displaying your Time struct property you can conform it to CustomStringConvertible and provide a custom description
extension Time: CustomStringConvertible {
var description: String {
String(format: "%02d:%02d", hour, minute)
}
}
Regarding adding and subtracting you can make your Time struct conform to AdditiveArithmetic and implement the required operators:
extension Time: AdditiveArithmetic {
static func - (lhs: Time, rhs: Time) -> Time {
let minutes = lhs.minute - rhs.minute + lhs.hour * 60 - rhs.hour * 60
return .init(hour: minutes/60, minute: minutes%60)
}
static func + (lhs: Time, rhs: Time) -> Time {
let minutes = lhs.minute + rhs.minute + lhs.hour * 60 + rhs.hour * 60
return .init(hour: minutes/60, minute: minutes%60)
}
static var zero: Time { .init(hour: 0, minute: 0) }
}
Playground testing:
let time1 = Time(string: "22:00")!
let time2 = Time(string: "20:00")!
let time3 = time1-time2
print(time3)
let time4 = time1+time2
print(time4)
Those will print
02:00
42:00
First we create the calculateDifference function which uses dateComponents function provided by Swift.
let time1 = "22:00"
let time2 = "20:00"
func formattedTime(_ time: String) -> Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter.date(from: time)
}
func calculateDifference(_ from: String, _ to: String) -> (hour: Int, minutes: Int) {
guard let fromTime = formattedTime(from),
let toTime = formattedTime(to) else {
return (0,0)
}
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: fromTime, to: toTime)
return (components.hour ?? 0, components.minute ?? 0)
}
Then we create the calculateSum which uses the calculateDifference internally to get the hours by supplying the default hour as 00:00
func calculateSum(_ time1: String, _ time2: String) -> (hour: Int, minutes: Int) {
let defaultTime = "00:00"
let calculatedTime1 = calculateDifference(defaultTime, time1)
let calcaultedTime2 = calculateDifference(defaultTime, time2)
return(calculatedTime1.hour + calcaultedTime2.hour,
calculatedTime1.minutes + calcaultedTime2.minutes)
}
Now if we run the below, we get the required results. Of course, some formatting will be required.
let difference = calculateDifference(time2, time1)
print("\(difference.hour):\(difference.minutes)")
let sum = calculateSum(time1, time2)
print("\(sum.hour):\(sum.minutes)")

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 :)

Working out the start and end of a day. Swift

I have a function to work out the start and end of a week which works as expected. I want to implement another function which works out the start and end of a single day. I have the code below however I get the following error:
Type of expression is ambiguous without more context.
public class Date {
let dateFormatter = NSDateFormatter()
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
func calcStartAndEndDateForWeek(durationOccurance: Double) {
print("Calculating start and end for week")
let componentsWeek = calendar.components([.YearForWeekOfYear, .WeekOfYear], fromDate: date)
let startOfWeek = calendar.dateFromComponents(componentsWeek)!
print("start of Week = \(dateFormatter.stringFromDate(startOfWeek))")
let componentsWeekEnds = NSDateComponents()
componentsWeekEnds.weekOfYear = 1
let endOfWeek = calendar.dateByAddingComponents(componentsWeekEnds, toDate: startOfWeek, options: [])!
print("End of the week = \(dateFormatter.stringFromDate(endOfWeek))")
}
func calcStartAndEndDateForDay(durationOccurance: Double) {
print("Calculating start and end for day")
let componentsWeek = calendar.components([.Minutes, .Seconds], fromDate: date)
let startOfDay = calendar.dateFromComponents(componentsWeek)!
print("start day = \(dateFormatter.stringFromDate(startOfDay))")
}
init(){
dateFormatter.dateFormat = "dd-MM-yyyy"
}
}
We can create a more generic function using the methods on NSCalendar:
func rangeOfPeriod(period: NSCalendarUnit, date: NSDate) -> (NSDate, NSDate) {
let calendar = NSCalendar.currentCalendar()
var startDate: NSDate? = nil
// let's ask calendar for the start of the period
calendar.rangeOfUnit(period, startDate: &startDate, interval: nil, forDate: date)
// end of this period is the start of the next period
let endDate = calendar.dateByAddingUnit(period, value: 1, toDate: startDate!, options: [])
// you can subtract 1 second if you want to make "Feb 1 00:00:00" into "Jan 31 23:59:59"
// let endDate2 = calendar.dateByAddingUnit(.Second, value: -1, toDate: endDate!, options: [])
return (startDate!, endDate!)
}
Called as
print("\(rangeOfPeriod(.WeekOfYear, date: NSDate()))")
print("\(rangeOfPeriod(.Day, date: NSDate()))")
Putting it into your code:
public class Date {
let dateFormatter = NSDateFormatter()
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
func rangeOfPeriod(period: NSCalendarUnit) -> (NSDate, NSDate) {
var startDate: NSDate? = nil
calendar.rangeOfUnit(period, startDate: &startDate, interval: nil, forDate: date)
let endDate = calendar.dateByAddingUnit(period, value: 1, toDate: startDate!, options: [])
return (startDate!, endDate!)
}
func calcStartAndEndDateForWeek() {
let (startOfWeek, endOfWeek) = rangeOfPeriod(.WeekOfYear)
print("Start of week = \(dateFormatter.stringFromDate(startOfWeek))")
print("End of the week = \(dateFormatter.stringFromDate(endOfWeek))")
}
func calcStartAndEndDateForDay() {
let (startOfDay, endOfDay) = rangeOfPeriod(.Day)
print("Start of day = \(dateFormatter.stringFromDate(startOfDay))")
print("End of the day = \(dateFormatter.stringFromDate(endOfDay))")
}
init() {
dateFormatter.dateFormat = "dd-MM-yyyy"
}
}
let myDate = Date()
myDate.calcStartAndEndDateForWeek()
myDate.calcStartAndEndDateForDay()
I was implementing something similar and went the following route:
extension Date {
static var startOfToday: Date? {
let date = Date()
guard !date.isStartOfDay else { return date }
return date
.zero(out: .second)?
.zero(out: .minute)?
.zero(out: .hour)?
.addingTimeInterval(-24 * 60 * 60)
}
private func zero(out: Calendar.Component) -> Date? {
return Calendar.current
.date(bySetting: out, value: 0, of: self)
}
private var isStartOfDay: Bool {
let cal = Calendar.current
let hours = cal.component(.hour, from: self)
let minutes = cal.component(.minute, from: self)
let seconds = cal.component(.second, from: self)
return hours == 0 && minutes == 0 && seconds == 0
}
}
Setting a component to zero will increment the next bigger component. So just setting the hour to zero will push the date to the next day at 00:00, unless of course the hour is already at zero. So to make it work for any date we have to zero out the seconds, minutes and hours (in that order). And to make sure we don't end up at the beginning of yesterday we first check if all values aren't already at zero.
I realize this is kinda hacky and probably not the best way to go about this, but it seems to work well enough for my use-case at least.
Getting the end of the day can be built on top of this by just adding another day.