skip to Main Content

HKQuantity and HKUnit support all of the common units of measurement, SI and otherwise, such as gram(), pound(), and stone().

The HealthKit classes also support custom complex units such as "foot-pound", "meters-per-second", or "meters-per-second-squared" via unitMultiplied(by:), unitDivided(by:), and unitRaised(toPower:)

The unit class supports most standard SI units (meters, seconds, and grams), SI units with prefixes (centimeters, milliseconds and kilograms) and equivalent non-SI units (feet, minutes, and pounds). HealthKit also supports creating complex units by mathematically combining existing units.

Does HealthKit, though, support completely custom units of measurement to model simple non-complex measurements like mass, length, or volume?

Imagine an MIT student project for a bridge-walking pedometer app. The app would certainly need to calculate and display distances in Smoot units.

let smoot = HKQuantity(unit: .meter(), doubleValue: 1.7018)
let meter = HKQuantity(unit: .smoot(), doubleValue: 1)

extension HKUnit {
    class func smoot() -> Self {
        // ???
    }
}

How can a smoot() be modeled in HealthKit HKUnits?

2

Answers


  1. Chosen as BEST ANSWER

    HealthKit does not currently seem to support custom HKUnits when creating HKQuantitys. You are limited to the custom HKUnit class functions Apple provides, such as .gram() and .cupImperial().

    HKQuantity does, though, provide a first-class unit converter. You can input and output values in any unit that Apple provides.

    This allows us to write an HKCustomUnit layer on top of the existing HealthKit units with no need to pull in the Measurement API from Foundation:

    extension HKQuantity {
        
        enum HKCustomUnit {
            case smoot;                   static func smoot()                   -> Self { .smoot }
            case teaspoonUS;              static func teaspoonUS()              -> Self { .teaspoonUS }
            case tablespoonUS;            static func tablespoonUS()            -> Self { .tablespoonUS }
            case tablespoonInternational; static func tablespoonInternational() -> Self { .tablespoonInternational }
            case tablespoonAustralia;     static func tablespoonAustralia()     -> Self { .tablespoonAustralia }
            case degreesFahrenheitCUSTOM; static func degreesFahrenheitCUSTOM() -> Self { .degreesFahrenheitCUSTOM }
            
            var conversion: (toUnit: HKUnit, coefficient: Double, constant: Double) {
                switch self {
                case .smoot: (.inch(), 67.0, 0)
                case .teaspoonUS: (.fluidOunceUS(), 1/6, 0)
                case .tablespoonUS: (.fluidOunceUS(), 1/2, 0)
                case .tablespoonInternational: (.literUnit(with: .milli), 15.0, 0)
                case .tablespoonAustralia: (.literUnit(with: .milli), 20.0, 0)
                case .degreesFahrenheitCUSTOM: (.degreeCelsius(), 5/9, -32)
                }
            }
        }
        
        convenience init(unit: HKCustomUnit, doubleValue value: Double) {
            self.init(unit: unit.conversion.toUnit, doubleValue: unit.conversion.coefficient * (value + unit.conversion.constant))
        }
        
        func doubleValue(for unit: HKCustomUnit) -> Double {
            (doubleValue(for: unit.conversion.toUnit) / unit.conversion.coefficient) - unit.conversion.constant
        }
    }
    

    With the extension function names based on the existing HKQuantity API, using HKCustomUnit is identical to using the provided HKUnit:

    let smoot1 = HKQuantity(unit: .smoot(), doubleValue: 1)
    let inches = smoot1.doubleValue(for: .inch())  // 67
    let meters = smoot1.doubleValue(for: .meter()) // 1.7018
    
    let inches134 = HKQuantity(unit: .inch(), doubleValue: 134)
    let smoots = inches134.doubleValue(for: .smoot()) // 2
    

  2. The point of HealthKit sharing is to allow different apps to be able to contribute and make use of health data. For this to work, the data recorded in the HealthKit store needs to be in common units.

    If you were to record a distance in Smoots then that would meaningless to any other app that read the data.

    Displaying data in your app is a different story. You can define your own UnitLength for Smoot and use that to convert Meters to Smoots for display or convert entered Smoots to Meters for storage:

    extension UnitLength {
        static var smoot: UnitLength = {
            let converter = UnitConverterLinear(coefficient: 1.7)
            return UnitLength(symbol: "Smoot", converter: converter)
        }()
    }
    
    let distance = Measurement(value: 2, unit: UnitLength.meters)
    
    let smoots = distance.converted(to: UnitLength.smoot)
    print(smoots)
    
    let meters = smoots.converted(to: UnitLength.meters)
    print(meters)
    

    This gives the output:

    1.1764705882352942 Smoot

    2.0 m

    The short answer is you can’t store custom units in HealthKit, but you don’t need to – You handle them with input/output, not storage.

    You cannot create your own HKUnit – From the documentation

    Using Units

    Like many HealthKit classes, the HKUnit class is not extendable and should not be subclassed.

    Using the UnitLength extension above you could create some convenience functions to convert to and from HKQuantity for you

    extension HKQuantity {
        
        static func smootQuantity(_ smoots: Double) -> HKQuantity {
            HKQuantity(unit: .meter(), doubleValue: Measurement(value: smoots, unit: UnitLength.smoot).converted(to: .meters).value)
        }
        
        func smootValue()->Double {
            Measurement(value:self.doubleValue(for: .meter()), unit: UnitLength.meters).converted(to: .smoot).value
        }
    }
    
    let quantity = HKQuantity.smootQuantity(1.0)
    let value = quantity.smootValue()
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search