Determining crash on HKObject _validateForCreation

While testing on device (Apple Watch) attempting to save an

HKWorkout
into
HealthKit
I am adding samples of distance samples, calories, heart rates and vo2Max to the workout. Unfortunately unlike this question I am not getting as detailed as a trace back...as far as I can tell it's crashing on adding a sample but I can't tell which sample it is or why?


Code:


private func addSamples(toWorkout workout: HKWorkout, from startDate: Date, to endDate: Date, handler: @escaping (Bool, Error?) -> Void) {




        let vo2MaxSample = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.vo2Max)!, quantity: vo2MaxQuantity(), start: startDate, end: endDate)


        var samples = [HKQuantitySample]()


        for distanceWalkingRunningSample in distanceWalkingRunningSamples {
            samples.append(distanceWalkingRunningSample)
        }


        for energySample in energySamples {
            samples.append(energySample)
        }


        samples.append(vo2MaxSample)
        samples.append(contentsOf: heartRateValues)


        // Add samples to workout
        healthStore.add(samples, to: workout) { (success: Bool, error: Error?) in


            if error != nil {
                print("Adding workout subsamples failed with error: \(String(describing: error))")
                handler(false, error)
            }


            if success {
                print("Success, samples have been added, workout Saved.")  //WorkoutStartDate = \(workout.startDate) WorkoutEndDate = \(workout.endDate)
                handler(true, nil)


            } else {
                print("Adding workout subsamples failed no error reported")
                handler(false, nil)
            }


        }
    }


Trace:


Exception Type: EXC_CRASH (SIGABRT)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note: EXC_CORPSE_NOTIFY

Triggered by Thread: 0



Application Specific Information:

abort() called



Filtered syslog:

None found



Last Exception Backtrace:

0 CoreFoundation 0x1bdf75e8 __exceptionPreprocess + 124

1 libobjc.A.dylib 0x1b15717c objc_exception_throw + 33

2 CoreFoundation 0x1bdf752c +[NSException raise:format:] + 103

3 HealthKit 0x273dbdde -[HKObject _validateForCreation] + 111

4 HealthKit 0x273dbc48 +[HKObject _newDataObjectWithMetadata:device:config:] + 219

5 HealthKit 0x273dbb30 +[HKSample _newSampleWithType:startDate:endDate:device:metadata:config:] + 159

6 HealthKit 0x273e9ba8 +[HKWorkout _workoutWithActivityType:startDate:endDate:workoutEvents:duration:totalActiveEnergyBurned:totalBasalEnergyBurned:totalDistance:totalSwimmingStrokeCount:totalFlightsClimbed:goalType:goal:device:metadata:config:] + 431

7 HealthKit 0x274a9342 +[HKWorkout workoutWithActivityType:startDate:endDate:workoutEvents:totalEnergyBurned:totalDistance:device:metadata:] + 109

8 HealthKit 0x274a9160 +[HKWorkout workoutWithActivityType:startDate:endDate:workoutEvents:totalEnergyBurned:totalDistance:metadata:] + 87





Thread 0 name: Dispatch queue: com.apple.main-thread

Thread 0 Crashed:

0 libsystem_kernel.dylib 0x1b9e443c __pthread_kill + 8

1 libsystem_pthread.dylib 0x1baec270 pthread_kill$VARIANT$mp + 334

2 libsystem_c.dylib 0x1b96d28e abort + 106

3 libc++abi.dylib 0x1b136cfe __cxa_bad_cast + 0

4 libc++abi.dylib 0x1b136e8a default_unexpected_handler+ 16010 () + 0

5 libobjc.A.dylib 0x1b1573e0 _objc_terminate+ 29664 () + 102

6 libc++abi.dylib 0x1b1493fc std::__terminate(void (*)+ 91132 ()) + 6

7 libc++abi.dylib 0x1b148ed6 __cxxabiv1::exception_cleanup_func+ 89814 (_Unwind_Reason_Code, _Unwind_Exception*) + 0

8 libobjc.A.dylib 0x1b157274 _objc_exception_destructor+ 29300 (void*) + 0

9 CoreFoundation 0x1bdf7530 -[NSException initWithCoder:] + 0

10 HealthKit 0x273dbde2 -[HKObject _validateForCreation] + 116

11 HealthKit 0x273dbc4c +[HKObject _newDataObjectWithMetadata:device:config:] + 224

12 HealthKit 0x273dbb34 +[HKSample _newSampleWithType:startDate:endDate:device:metadata:config:] + 164

13 HealthKit 0x273e9bac +[HKWorkout _workoutWithActivityType:startDate:endDate:workoutEvents:duration:totalActiveEnergyBurned:totalBasalEnergyBurned:totalDistance:totalSwimmingStrokeCount:totalFlightsClimbed:goalType:goal:device:metadata:config:] + 436

14 HealthKit 0x274a9346 +[HKWorkout workoutWithActivityType:startDate:endDate:workoutEvents:totalEnergyBurned:totalDistance:device:metadata:] + 114

15 HealthKit 0x274a9164 +[HKWorkout workoutWithActivityType:startDate:endDate:workoutEvents:totalEnergyBurned:totalDistance:metadata:] + 92

Replies

A year old and no replies--well I can say I'm seeing similar crash logs from my TestFlight beta users. No details in the stack trace to what is failing validation.


From Apple's documentation there are a few reasons it could throw: https://developer.apple.com/documentation/healthkit/hkquantitysample/1615019-init


Here is my invocation of the init. Decided to put add the guard on line 10 just in case CoreMotion is giving me junk dates for start and end date of pedometerData I say "what gives"?


private extension HKQuantitySample {
    convenience init?(pedometerData: CMPedometerData, device: HKDevice? = nil, metadata: [String: Any]? = nil) {
        
        guard let distance = pedometerData.distance?.doubleValue, distance > 0.0 else { return nil }
        let type: HKQuantityType = HKWorkoutType.distanceWalkingRunningType() // Extension that force unwraps
        let quantity: HKQuantity = HKQuantity(unit: .meter(), doubleValue: distance)
        
        let startDate = pedometerData.startDate
        let endDate = pedometerData.endDate
        guard endDate > startDate else { return nil }
        
        // This is what throws for me
        self.init(type: type, quantity: quantity, start: startDate, end: endDate, device: device, metadata: metadata)
    }
}

Happy hunting!


I've just recently discovered a set of new functions on HKSampleType that may shed some light on this.


iOS 13 and watchOS 6 added some new API around minimum and maximum supported time durations for a given type. Perhaps, the samples both of us were making were invalid durations and it was a previously undocument/unknown validation that was occurring.


https://developer.apple.com/documentation/healthkit/hksampletype?changes=latest_minor

https://developer.apple.com/documentation/healthkit/hksampletype/3327021-minimumallowedduration

Since I needed some time to figure the implementation out, here is a code example from my workout app "Progression" on how to handle the duration limits:

private func createAndStoreHealthKitWorkout(start: Date, end: Date, workoutName: String) {
        let device = HKDevice(name: UIDevice.current.localizedModel,
                              manufacturer: Constants.deviceManufacturer,
                              model: UIDevice.current.localizedModel,
                              hardwareVersion: modelIdentifier(),
                              firmwareVersion: nil,
                              softwareVersion: UIDevice.current.systemVersion,
                              localIdentifier: nil,
                              udiDeviceIdentifier: nil)

		let maximumAllowedWorkoutDuration = 345600.0
		let endDate: Date

		if #available(iOS 13, *) {
			let timeIntervalAdjustedForMinimumLimit = HKObjectType.workoutType().isMinimumDurationRestricted ?
				max(
					end.timeIntervalSince(start),
					HKObjectType.workoutType().minimumAllowedDuration
				) : end.timeIntervalSince(start)

			let timeIntervalAdjustedForDurationLimits = HKObjectType.workoutType().isMaximumDurationRestricted ?
				min(
					timeIntervalAdjustedForMinimumLimit,
					HKObjectType.workoutType().maximumAllowedDuration
				) : timeIntervalAdjustedForMinimumLimit

				endDate = Date(timeInterval: timeIntervalAdjustedForDurationLimits, since: start)
		} else {
			endDate = Date(timeInterval: maximumAllowedWorkoutDuration, since: start)
		}

		let workoutEntry = HKWorkout(
			activityType: .traditionalStrengthTraining,
			start: start,
			end: endDate,
			workoutEvents: nil,
			totalEnergyBurned: nil,
			totalDistance: nil,
			totalSwimmingStrokeCount: nil,
			device: device,
			metadata: [Constants.workoutNameMetadataKey: workoutName]
		)

        healthStore?.save(workoutEntry, withCompletion: { (_, _) in })
}

Hope I saved someones time.

Note that the hardcoded "maximumAllowedWorkoutDuration" (probably not needed for <iOS 13 versions) is the limit for workout object types.

This was so helpful, thank you for posting the answer with such limited engagement here for so long.