Compare commits

...

2 Commits

Author SHA1 Message Date
Roberto Viola
8a3b211edf Update project.pbxproj 2025-09-27 06:54:29 +02:00
Roberto Viola
4b3ea5a1d0 Airpods Pro 3 Heart Rate 2025-09-26 15:56:20 +02:00
2 changed files with 122 additions and 60 deletions

View File

@@ -4455,7 +4455,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4655,7 +4655,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -4891,7 +4891,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -4987,7 +4987,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5079,7 +5079,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5195,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1167;
CURRENT_PROJECT_VERSION = 1168;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;

View File

@@ -29,20 +29,24 @@ protocol WorkoutTrackingProtocol {
@objc class WorkoutTracking: NSObject {
static let shared = WorkoutTracking()
public static var lastDateMetric = Date()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var steps = Double()
var sport: Int = 0
let healthStore = HKHealthStore()
let configuration = HKWorkoutConfiguration()
var workoutBuilder: HKWorkoutBuilder!
var workoutInProgress: Bool = false
weak var delegate: WorkoutTrackingDelegate?
override init() {
super.init()
public static var distance = Double()
public static var kcal = Double()
public static var totalKcal = Double()
public static var steps = Double()
var sport: Int = 0
let healthStore = HKHealthStore()
let configuration = HKWorkoutConfiguration()
var workoutBuilder: HKWorkoutBuilder!
var workoutInProgress: Bool = false
private var heartRateQuery: HKAnchoredObjectQuery?
private var heartRateQueryAnchor: HKQueryAnchor?
private let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
private let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)
weak var delegate: WorkoutTrackingDelegate?
override init() {
super.init()
}
}
@@ -52,12 +56,12 @@ extension WorkoutTracking {
self.sport = sport
}
private func configWorkout() {
var activityType = HKWorkoutActivityType.cycling
if self.sport == 1 {
activityType = HKWorkoutActivityType.running
} else if self.sport == 2 {
activityType = HKWorkoutActivityType.cycling
private func configWorkout() {
var activityType = HKWorkoutActivityType.cycling
if self.sport == 1 {
activityType = HKWorkoutActivityType.running
} else if self.sport == 2 {
activityType = HKWorkoutActivityType.cycling
} else if self.sport == 3 {
activityType = HKWorkoutActivityType.rowing
} else if self.sport == 4 {
@@ -67,16 +71,71 @@ extension WorkoutTracking {
configuration.activityType = activityType
configuration.locationType = .indoor
do {
workoutBuilder = try HKWorkoutBuilder(healthStore: healthStore, configuration: configuration, device: .local())
} catch {
return
}
}
}
@available(iOS 17.0, *)
extension WorkoutTracking: WorkoutTrackingProtocol {
do {
workoutBuilder = try HKWorkoutBuilder(healthStore: healthStore, configuration: configuration, device: .local())
} catch {
return
}
}
private func startHeartRateStreamingQuery() {
guard heartRateQuery == nil else { return }
guard HKHealthStore.isHealthDataAvailable() else {
SwiftDebug.qtDebug("WorkoutTracking: Health data unavailable for heart rate query")
return
}
guard let heartRateType = heartRateType else {
SwiftDebug.qtDebug("WorkoutTracking: Heart rate type unavailable")
return
}
let predicate = HKQuery.predicateForSamples(withStart: Date(), end: nil, options: .strictStartDate)
let query = HKAnchoredObjectQuery(type: heartRateType, predicate: predicate, anchor: heartRateQueryAnchor, limit: HKObjectQueryNoLimit) { [weak self] _, samples, _, newAnchor, error in
self?.handleHeartRateSamples(samples, error: error)
self?.heartRateQueryAnchor = newAnchor
}
query.updateHandler = { [weak self] _, samples, _, newAnchor, error in
self?.handleHeartRateSamples(samples, error: error)
self?.heartRateQueryAnchor = newAnchor
}
heartRateQuery = query
healthStore.execute(query)
}
private func stopHeartRateStreamingQuery() {
if let query = heartRateQuery {
healthStore.stop(query)
}
heartRateQuery = nil
heartRateQueryAnchor = nil
}
private func handleHeartRateSamples(_ samples: [HKSample]?, error: Error?) {
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking: Heart rate query error " + error.localizedDescription)
return
}
guard
let quantitySamples = samples as? [HKQuantitySample],
let latestSample = quantitySamples.last
else {
return
}
let bpm = latestSample.quantity.doubleValue(for: heartRateUnit)
DispatchQueue.main.async {
WatchKitConnection.currentHeartRate = Int(round(bpm))
self.delegate?.didReceiveHealthKitHeartRate(bpm)
}
}
}
@available(iOS 17.0, *)
extension WorkoutTracking: WorkoutTrackingProtocol {
@objc static func requestAuth() {
authorizeHealthKit()
@@ -141,31 +200,34 @@ extension WorkoutTracking: WorkoutTrackingProtocol {
}
}
@objc func startWorkOut(deviceType: UInt16) {
if(workoutInProgress) {
return;
@objc func startWorkOut(deviceType: UInt16) {
if(workoutInProgress) {
return;
}
WorkoutTracking.authorizeHealthKit()
workoutInProgress = true;
WorkoutTracking.lastDateMetric = Date()
SwiftDebug.qtDebug("WorkoutTracking: Start workout")
setSport(Int(deviceType))
configWorkout()
startHeartRateStreamingQuery()
workoutBuilder.beginCollection(withStart: Date()) { (success, error) in
SwiftDebug.qtDebug(success.description)
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking: " + error.localizedDescription)
}
}
WorkoutTracking.authorizeHealthKit()
workoutInProgress = true;
WorkoutTracking.lastDateMetric = Date()
SwiftDebug.qtDebug("WorkoutTracking: Start workout")
setSport(Int(deviceType))
configWorkout()
workoutBuilder.beginCollection(withStart: Date()) { (success, error) in
SwiftDebug.qtDebug(success.description)
if let error = error {
SwiftDebug.qtDebug("WorkoutTracking: " + error.localizedDescription)
}
}
}
@objc func stopWorkOut() {
SwiftDebug.qtDebug("WorkoutTracking: Stop workout")
guard let workoutBuilder = self.workoutBuilder,
let startDate = workoutBuilder.startDate else {
SwiftDebug.qtDebug("WorkoutTracking: Cannot stop workout - no workout builder or start date available")
workoutInProgress = false
}
@objc func stopWorkOut() {
SwiftDebug.qtDebug("WorkoutTracking: Stop workout")
stopHeartRateStreamingQuery()
guard let workoutBuilder = self.workoutBuilder,
let startDate = workoutBuilder.startDate else {
SwiftDebug.qtDebug("WorkoutTracking: Cannot stop workout - no workout builder or start date available")
workoutInProgress = false
return
}