3

My watchos app relies on haptic feedback working in the background. I have searched and searched over the internet on how to do this and have only learnt how to add things to my info.plist

I do not want to use notifications to alert my user as its to cumbersome in terms of UI because this would be done too often.

I read about workout sessions but can't get this to work.

How do I allow haptic feedback to work while the wrist is lowered? The simplest way will be enough and I am not the most advanced with swift so please try explain fully!

An example of my code is below. Please show me where to put it if I want background haptic feedback to work:

My timer:

 _ = Timer.scheduledTimer(timeInterval: 88, target: self, selector: "action", userInfo: nil, repeats: true)

My action:

 func action() {
    print("action")
    WKInterfaceDevice.current().play(.success)
     imageObject.setImageNamed("number2")     
}

This is what I'm doing with HKWorkout session but have absolute no idea whats going on.

  func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
    switch toState {
    case .running:
        workoutDidStart(date)
    case .ended:
        workoutDidEnd(date)
    default:
        print("Unexpected state \(toState)")
    }
}

func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
    // Do nothing for now
    print("Workout error")
}


func workoutDidStart(_ date : Date) {
    if let query = createHeartRateStreamingQuery(date) {
        self.currenQuery = query
        healthStore.execute(query)
    } else {
        label.setText("cannot start")
    }
}

func workoutDidEnd(_ date : Date) {
    healthStore.stop(self.currenQuery!)
    label.setText("---")
    session = nil
}

// MARK: - Actions
@IBAction func startBtnTapped() {
    if (self.workoutActive) {
        //finish the current workout
        self.workoutActive = false
        self.startStopButton.setTitle("Start")
        if let workout = self.session {
            healthStore.end(workout)
        }
    } else {
        //start a new workout
        self.workoutActive = true
        self.startStopButton.setTitle("Stop")
        _ = Timer.scheduledTimer(timeInterval: 5, target: self, selector: "firsts", userInfo: nil, repeats: false)


        startWorkout()
    }

}

func startWorkout() {

    // If we have already started the workout, then do nothing.
    if (session != nil) {
        return
    }

    // Configure the workout session.
    let workoutConfiguration = HKWorkoutConfiguration()
    workoutConfiguration.activityType = .crossTraining
    workoutConfiguration.locationType = .indoor

    do {
        session = try HKWorkoutSession(configuration: workoutConfiguration)
        session?.delegate = self
    } catch {
        fatalError("Unable to create the workout session!")
    }

    healthStore.start(self.session!)
}

func createHeartRateStreamingQuery(_ workoutStartDate: Date) -> HKQuery? {


    guard let quantityType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else { return nil }
    let datePredicate = HKQuery.predicateForSamples(withStart: workoutStartDate, end: nil, options: .strictEndDate )
    //let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
    let predicate = NSCompoundPredicate(andPredicateWithSubpredicates:[datePredicate])


    let heartRateQuery = HKAnchoredObjectQuery(type: quantityType, predicate: predicate, anchor: nil, limit: Int(HKObjectQueryNoLimit)) { (query, sampleObjects, deletedObjects, newAnchor, error) -> Void in
        //guard let newAnchor = newAnchor else {return}
        //self.anchor = newAnchor
        self.updateHeartRate(sampleObjects)
    }

    heartRateQuery.updateHandler = {(query, samples, deleteObjects, newAnchor, error) -> Void in
        //self.anchor = newAnchor!
        self.updateHeartRate(samples)
    }
    return heartRateQuery
}

func updateHeartRate(_ samples: [HKSample]?) {
    guard let heartRateSamples = samples as? [HKQuantitySample] else {return}

    DispatchQueue.main.async {
        guard let sample = heartRateSamples.first else{return}
        let value = sample.quantity.doubleValue(for: self.heartRateUnit)
        self.label.setText(String(UInt16(value)))

        // retrieve source from sample
        let name = sample.sourceRevision.source.name
        self.updateDeviceName(name)
        self.animateHeart()
    }
}

func updateDeviceName(_ deviceName: String) {
    deviceLabel.setText(deviceName)
}

func animateHeart() {
    self.animate(withDuration: 0.5) {
        self.heart.setWidth(60)
        self.heart.setHeight(90)
    }

    let when = DispatchTime.now() + Double(Int64(0.5 * double_t(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)

    DispatchQueue.global(qos: .default).async {
        DispatchQueue.main.asyncAfter(deadline: when) {
            self.animate(withDuration: 0.5, animations: {
                self.heart.setWidth(50)
                self.heart.setHeight(80)
            })            }


    }
}

Look I don't even need heart rate data or anything at all but the taptic engine buzzing in the background.

Mr Big Problem
  • 269
  • 1
  • 3
  • 12

2 Answers2

4

As an addition to my previous answer I created a sample project that shows how to trigger vibrations while in the background:

For a full example on how to use HKWorkoutSession check out my sample project on GitHub. The sample app will trigger a vibration every five seconds even with the app running in the background. HKWorkoutSession and thus the sample only work when the app was signed with a provisioning profile containing the HealthKit entitlement. Be sure to change the Development Team to your own Team for all three available targets. Xcode will try to create the needed provisioning profiles. If there are any signing issues or you use a wildcard provisioning profile running in the background will not work.

Community
  • 1
  • 1
naglerrr
  • 2,809
  • 1
  • 12
  • 24
  • Thank you so much for taking the time to create this and assisting me. Enjoy your bounty! I have a couple of questions though. Is it possible to disable the heart rate sensor while using the app? And how do you end a workout? Thanks again for the help. – Mr Big Problem Mar 05 '17 at 13:58
  • I have a more important question, hopefully you can answer. What is the significance of using private and file private functions in this context? How would I stop the timer in the extension delegate: func applicationWillResignActive() if it is a private function? – Mr Big Problem Mar 23 '17 at 18:46
  • This is just an example. The methods do not need to be `private` or `fileprivate`. – naglerrr Mar 24 '17 at 09:14
2

This is only possible using HKWorkoutSessions. Apple's App Programming Guide for watchOS clearly specifies the available background modes without a HKWorkoutSession and none of them will allow you to trigger haptic feedback while in the background.

Using HKWorkoutSession you can implement apps that track the users workout using an app. While a HKWorkoutSession is running your app has several privileges:

The app continues to run throughout the entire workout session, even when the user lowers their wrist or interacts with a different app. When the user raises their wrist, the app reappears, letting the user quickly and easily check their current progress and performance.

The app can continue to access data from Apple Watch’s sensors in the background, letting you keep the app up to date at all times. For example, a running app can continue to track the user’s heart rate, ensuring that the most recent heart rate data is displayed whenever the user raises their wrist.

The app can alert the user using audio or haptic feedback while running in the background.

To make use of HKWorkoutSession and provide haptic feedback you will need to add the WKBackgroundModes key and the UIBackgroundModes to your WatchKit extension’s Info.plist.

<key>WKBackgroundModes</key>
<array>
    <string>workout-processing</string>
</array>
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

There are several caveats to this:

  • When another WKWorkoutSession is started by another app your session will be ended
  • If you are using resources excessively while in the background your app may be suspended by watchOS
  • Your app may contribute to filling the activity rings in the Activity App on the paired iPhone
  • Depending on your application type Apple may reject your app when publishing in the App Store
  • The HealthKit entitlement is needed (You can add it in the Capabilities view in Xcode)

For a more detailed guide on how to implement HKWorkoutSession check out the API Reference.

For a full example on how to use HKWorkoutSession check out my sample project on GitHub. The sample app will trigger a vibration every five seconds even with the app running in the background. HKWorkoutSession and thus the sample only work when the app was signed with a provisioning profile containing the HealthKit entitlement. Be sure to change the Development Team to your own Team for all three available targets. Xcode will try to create the needed provisioning profiles. If there are any signing issues or you use a wildcard provisioning profile running in the background will not work.

naglerrr
  • 2,809
  • 1
  • 12
  • 24