I'm looking for a way to detect workout detect / stop using HealthKit and it seems there is no way to detect it.
In Android you get "ACTION_SESSION_START" and "ACTION_SESSION_END" for sessions.
Has anyone tried detecting workout start / stop?
Thank you for your time!
There is no API for observing workouts being recorded by another app.
I was thinking you could detect events. Not save a workout.
In HealthKit you can create workout events. Therefore you could theoretically code something to detect pause/resume events.
Create a running workout that goes for ___ time.
let finish = NSDate() // Now
let start = finish.dateByAddingTimeInterval(0000) // workout time
let workout = HKWorkout(activityType: .Running, startDate: start, endDate: finish)
Create Pause (stop) and Resume (start) events.
let workoutEvents: [HKWorkoutEvent] = [
HKWorkoutEvent(type: .Pause, date: startDate.dateByAddingTimeInterval(000)),
HKWorkoutEvent(type: .Resume, date: startDate.dateByAddingTimeInterval(000))
]
Then you need to alter the creation of the HKWorkout object to use the more complex constructor, which allows you to include workoutEvents.
let workout = HKWorkout(
activityType: .Running
startDate: start,
endDate: end,
workoutEvents: workoutEvents,
device: nil,
metadata: nil
)
At this point you would normally pass the workout to HKHealthStore.saveObject to save it like this.
healthStore.saveObject(workout) { (success: Bool, error: NSError?) -> Void in
if success {
// Workout was saved
}
else {
// Workout was not saved
}
}
But
in your case you don't want to save. You want to detect events. could have a switch statement that could detect a Pause or Resume.
I'm not sure about the specific dateByAddingTimeInterval values you would want but it's definitely something you could experiment with using zero values maybe? Because technically 00 is still an event.
Workout events toggle a workout object between an active and an inactive state. You could create a method detectWorkoutEvent.
Related
I am testing the SpeedySloth demo app from Apple: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/speedysloth_creating_a_workout
Well, this ends here, whenever a second app starts:
// MARK: - HKWorkoutSessionDelegate
extension WorkoutManager: HKWorkoutSessionDelegate {
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState, date: Date) {
// Wait for the session to transition states before ending the builder.
/// - Tag: SaveWorkout
if toState == .ended {
print("The workout has now ended.")
builder.endCollection(withEnd: Date()) { (success, error) in
self.builder.finishWorkout { (workout, error) in
// Optionally display a workout summary to the user.
self.resetWorkout()
}
}
}
}
The delegate is directly called with toState = .ended when I press the "Start" Button in Nike Running Club. I assume, that there is only one workout possible at one time, BUT I can use Adidas Running along with NRC, so, it must be somehow possible.
The documentation for HKWorkoutSession states that only one can be run at a time.
Apple Watch runs one workout session at a time. If a second workout starts while your workout is running, your HKWorkoutSessionDelegate object receives an HKError.Code.errorAnotherWorkoutSessionStarted error, and your session ends.
See https://developer.apple.com/documentation/healthkit/hkworkoutsession
Before the Apple Watch came out several Apps allowed users to measure a run or walk session by using the CoreMotion APIs. I suspect one of the Apps you mention may fall back to this if a Workout Session is already running.
My question is the following:
Can I change the HKWorkoutConfiguration.activityType during a
HKWorkoutSession or does each HKWorkoutSession has to have its own
HKWorkoutConfiguration.activityType?
I want to create a workout app where you can create a workout consisting of different sets with different activity types. For example a Shadowing Boxing Workout, consisting of 3 sets of Boxing and 3 sets of Kickboxing (Boxing and Kickboxing are the different activities).
Ideally I would just start the HKWorkoutSession once at the beginning and end it after all sets for each activity are done, changing the HKWorkoutConfiguration.activityType in-between.
My current approach is based on the sample provided by Apple: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/speedysloth_creating_a_workout
I adjusted the startWorkout() method to startWorkout(for type: String). It now looks like this:
// Start the workout.
func startWorkout(for type: String) {
// Start the timer.
setUpTimer()
self.running = true
// Create the session and obtain the workout builder.
/// - Tag: CreateWorkout
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: self.workoutConfiguration(for: type))
builder = session.associatedWorkoutBuilder()
} catch {
// Handle any exceptions.
return
}
// Setup session and builder.
session.delegate = self
builder.delegate = self
// Set the workout builder's data source.
/// - Tag: SetDataSource
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: workoutConfiguration(for: type))
// Start the workout session and begin data collection.
/// - Tag: StartSession
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { (success, error) in
// The workout has started.
}
print("New Workout has started")
}
In the method I get the respective activity by workoutConfiguration(for: type) which looks up the right activity from a string.
After a set is done (e.g. the boxing set), I end the session and start a new workout and session for the new set with the new activity.
My problem with the current approach is that I need to end the current HKWorkoutSession before I start the new one. But ending the session the way its done in the example does not execute immediately and therefore the new set of the workouts starts without saving the old set to the HKStore with the right activity.
Therefore I thought I would be nice to start the session just once and switch activityTypes in-between. However, I don't know if it is possible (maybe complications with HKStore) and how it is done.
Or is there any other smart way of doing things to achieve this?
I'm just starting out with iOS Programming.
Any help is greatly appreciated!
Disclaimer: This isn't a full answer, but an approach I'd take to address the problem.
Look at the workoutSession(_:didChangeTo:from:date:) method. Documentation Link
When one type of workout ends that method will receive a notification. As long as you haven't called session.end() the workout session should still be active. Use your healthStore instance to save the previous workout session before starting a new one.
It could look like this;
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
if toState == .ended {
workoutBuilder.endCollection(withEnd: date) { (success, error) in
// collection has ended for one type of workout
self.workoutBuilder.finishWorkout { (workout, error) in
// unwrap the optional `workout`
guard let completedWorkout = workout else { return }
// save workout to health store
healthStore.save(completedWorkout) { (success, error) in
if success {
// begin a new workout by calling your method to start a new workout with a new configuration here
startWorkout(for type: String)
}
}
}
}
}
}
That is likely going to cause issues with your Timer since you're calling setUpTimer again but you can work with that. Also this doesn't address the point when the user is completely done with their workout and doesn't want to start a new workout type.
I want to measure how long (in seconds) it takes users to do certain things in my app. Some examples are logging in, pressing a button on a certain page, etc.
I am using an NSTimer for that. I am starting it in the viewDidLoad of a specific page, and stopping it at the point that I want to measure.
I also want to measure cumulative time for certain things. I would like to start the timer on the log-in screen, and then continue the timer until the user gets to the next view controller and clicks on a certain button.
I'm not sure how to do this. Should create a global variable in my app delegate? Or is there some other better way?
No need for an NSTimer, you just need to record the start times and compare them to the stop times. Try using a little helper class such as:
class MyTimer {
static let shared = MyTimer()
var startTimes = [String : Date]()
func start(withKey key: String) {
startTimes[key] = Date()
}
func measure(key: String) -> TimeInterval? {
if let start = startTimes[key] {
return Date().timeIntervalSince(start)
}
return nil
}
}
To use this, just call start(withKey:) right before you start a long-running task.
MyTimer.shared.start(withKey: "login")
Do something that takes a while and then call measure(key:) when you're done. Because MyTimer is a singleton, it can be called from anywhere in your code.
if let interval = MyTimer.shared.measure("login") {
print("Logging in time: \(interval)")
}
If you're using multiple threads, you may to to add some thread safety to this, but it should work as is in simple scenarios.
I am trying to build a watchOS 2 complication that displays a user's health data, such as steps (but in theory it should be able to display any health data the user has given the app permission to view). When the complication first launches, I can query Healthkit and get all the data I want because the first launch is considered to be in the foreground. However, I am having trouble retrieving the HealthKit data in the background when new health data is available. There are two places I could get this data, the watch and the iPhone.
I have tried to get the data from the watch itself when the complication's background refresh is triggered from the date set in getNextRequestedUpdateDateWithHandler. However, when I call HKHealthStore's execute method, it does not return any query results if the app (or in this case the complication) is running the background. I have also tried to setup an HKAnchoredObject query that should return my results immediately when the process resumes, but this also doesn't seem to return any results unless I manually launch the app extension on the watch. Here is my watch code, called from my ExtensionDelegate's init method after health kit permissions are requested:
func setupComplicationDataCache() {
let now = NSDate()
var startDate: NSDate? = nil
var interval: NSTimeInterval = 0
self.calendar.rangeOfUnit(NSCalendarUnit.Day, startDate: &startDate, interval: &interval, forDate: now)
let stepSampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!
// Match samples with a start date after the workout start
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: nil, options: .None)
let query = HKAnchoredObjectQuery(type: stepSampleType, predicate: predicate, anchor: nil, limit: 0) { (query, samples, deletedObjects, anchor, error) -> Void in
// Handle when the query first returns results
self.handleStepResults(query, samples: samples, deletedObjects: deletedObjects, anchor: anchor, error: error)
}
query.updateHandler = { (query, samples, deletedObjects, anchor, error) -> Void in
// Handle update notifications after the query has initially run
self.handleStepResults(query, samples: samples, deletedObjects: deletedObjects, anchor: anchor, error: error)
}
self.healthStore.executeQuery(query);
}
func handleStepResults(query: HKAnchoredObjectQuery, samples: [HKSample]?, deletedObjects: [HKDeletedObject]?, anchor: HKQueryAnchor?, error: NSError?) {
if error != nil {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: -1), endDate: NSDate())
} else if samples == nil || samples?.count == 0 {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: 0), endDate: NSDate())
} else {
let newStepSamples = samples as! [HKQuantitySample]
var stepCount = self.timelineModel.currentEntry.value.doubleValue
var currentDate = self.timelineModel.currentEntry.endDate
// Add the current entry to the collection of past entries
self.timelineModel.pastEntries.append(self.timelineModel.currentEntry)
// Add all new entries to the collection of past entries
for result in newStepSamples {
stepCount += result.quantity.doubleValueForUnit(self.countUnit)
currentDate = result.endDate
self.timelineModel.pastEntries.append(TimelineEntryModel(value: NSNumber(double: stepCount), endDate: currentDate))
}
// Retrieve the latest sample as the current item
self.timelineModel.currentEntry = self.timelineModel.pastEntries.popLast()
if self.timelineModel.currentEntry == nil {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: -3), endDate: NSDate())
}
}
// Reload the complication
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
I have also tried to get the data from the iPhone using HKObserverQuery. I have the observer query that can wake up the iPhone once an hour (the max time for step data). However, if the iPhone is locked when the observer completion handler executes my step query, HKHealthStore's execute method also refuses to return any query results. I think this makes sense here and there is probably not a way around this because Apple's docs mention that the Health Store is encrypted when a device is locked and you can't read from the store (only write). BUT in the watch's case when it is on someones wrist it is not locked, the screen is just turned off.
Does anyone know how to get HealthKit updates to show up on a complication when a refresh happens in the background, either in iOS or on watchOS 2?
After extensive testing I have determined that this currently is not possible. On watchOS 2, Apple seems to have completely disabled HealthKit queries from returning results when an extension or complication is running in the background. This includes execution from remote notifications, Watch Connectivity, and from the complications scheduled refresh. The iPhone's HealthKit queries fail if the screen is off and the device has a passcode set. The queries fail because the health data store is encrypted when the device is locked. The queries fail even if observer queries and background delivery is enabled. You can get notified that something changed, but you can't query for the changes until the iPhone is unlocked (because again, the data is encrypted).
Other apps that show healthkit related data, such as steps and walk+run distance do so by querying the pedometer (CMPedometer) directly, whose data is accessible in these background modes.
One could make a complication that updates in the background exclusivly for iPhone users who do not have a passcode set on their device, but this seems like a terrible idea to promote.
I'm writing an app, that has to do some calculations every time the phone moves. I've read every question here, but couldn't get the Accelerometer to gather data in the background (after the user navigates away from the app). I've set the Location updates flag in the Plist.info. This is the code I'm using:
let motionManager = CMMotionManager()
func startMotionUpdates() {
var timestamps = [NSDate]()
if motionManager.accelerometerAvailable {
motionManager.accelerometerUpdateInterval = 1
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue(), withHandler: { (data: CMAccelerometerData?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let time = NSDate(timeIntervalSince1970: data!.timestamp)
timestamps.append(time)
print(timestamps)
})
})
}
}
I tried every combination out there, I've tried using CMDeviceMotion, I've tried using CLLocationManager.startUpdatingLocation() to simulate background activity, but nothing works. Does anybody have any ideas?
You cannot be woken up every time the accelerometer changes. You can be woken up whenever the location of the device changes significantly (at least several meters) using CLLocationManager, and that's what "location" means in the background modes.
To track finer-grained motion information, you need to ask the system to start recording the data using CMSensorRecorder, and then later you can ask for the data and compute what you want from it. But you won't be allowed to run in the background continuously watching every jiggle of the device. That would eat too much battery.
See also CMPedometer which addresses certain use cases more directly.