Exploring HealthKit: Working with State of Mind APIs

While I have been working on a lot of apps this summer, the one that I am most excited about is Arising, a mood and emotion tracker based on the new State of Mind APIs in HealthKit.

State of Mind in the Health app helps us to reflect on our current emotions that we are feeling, and our mood for the overall day. I dislike that we can log only one mood per day as if it is constant throughout, but we all know that change is the only constant.

According to the dictionary, emotion is a strong feeling deriving from one's circumstances, mood, or relationships with others. So, we can log such feelings multiple times per day. On the other hand, mood is a temporary state of mind or feeling. As I said earlier, it is a limitation that we can only log one mood per day.

And according to the Health team, the State of Mind APIs are designed in close collaboration with the Emotion Science experts. It has 4 parameters:

Kind

It is an enumeration that helps you provide the context of the feeling.

@available(iOS 18.0, *)
public enum Kind : Int, @unchecked Sendable {
  case momentaryEmotion = 1
  case dailyMood = 2
}

It has two cases:

  • dailyMood: As the name suggests, the kind of state over the day,
  • momentaryEmotion: The kind of state of how somebody feels in the moment.

Valence and ValenceClassification

It is a Double value to meaure someone’s feelings on a scale of -1.0 to 1.0. This value is what is represented by the slider on the Health app with the beautiful animations that goes from “Very Unplesant” to “Neutral” to “Very Pleasant”.

While valence provides with a value, there is an enumeration ValenceClassification that provides the cases of the different state of feeling. It ranges from "very unpleasant" to "very pleasant" with seven levels that you have seen in the Health app:

@available(iOS 18.0, *)
public enum ValenceClassification : Int, @unchecked Sendable {
  case veryUnpleasant = 1
  case unpleasant = 2
  case slightlyUnpleasant = 3
  case neutral = 4
  case slightlyPleasant = 5
  case pleasant = 6
  case veryPleasant = 7
}

What is important to note is that while the enum uses integer values internally, it is typically initialized with a Double value ranging from -1.0 to 1.0:

init?(valence: Double)

You will probably need the name, so let's make it conform to CustomStringConvertible:

extension HKStateOfMind.ValenceClassification: @retroactive CustomStringConvertible {
  public var description: String {
    switch self {
      case .veryUnpleasant: "Very Unpleasant"
      case .unpleasant: "Unpleasant"
      case .slightlyUnpleasant: "Slightly Unpleasant"
      case .neutral: "Neutral"
      case .slightlyPleasant: "Slightly Pleasant"
      case .pleasant: "Pleasant"
      case .veryPleasant: "Very Pleasant"
      @unknown default: "Neautral"
    }
  }
}

Labels

It is the enumeration with the cases about how someone feels. For example, amused, content, indifferent, peaceful, etc. It has numerous cases, a total of 38 at the time of writing this post:

@available(iOS 18.0, *)
public enum Label: Int, @unchecked Sendable {
  case amazed = 1
  case amused = 2
  case angry = 3
  case anxious = 4
  case ashamed = 5
  case brave = 6
  case calm = 7
  case content = 8
  case disappointed = 9
  case discouraged = 10
  case disgusted = 11
  case embarrassed = 12
  case excited = 13
  case frustrated = 14
  case grateful = 15
  case guilty = 16
  case happy = 17
  case hopeless = 18
  case irritated = 19
  case jealous = 20
  case joyful = 21
  case lonely = 22
  case passionate = 23
  case peaceful = 24
  case proud = 25
  case relieved = 26
  case sad = 27
  case scared = 28
  case stressed = 29
  case surprised = 30
  case worried = 31
  case annoyed = 32
  case confident = 33
  case drained = 34
  case hopeful = 35
  case indifferent = 36
  case overwhelmed = 37
  case satisfied = 38
}

As they are integer values, you probably will need to have the names for each of the label. Here is the extension on HKStateOfMind.Label that conforms to CaseIterable:

extension HKStateOfMind.Label: @retroactive CaseIterable, @retroactive CustomStringConvertible {
  public var description: String {
    switch self {
      case .amazed: "Amazed"
      case .amused: "Amused"
      case .angry: "Angry"
      case .anxious: "Anxious"
      case .ashamed: "Ashamed"
      case .brave: "Brave"
      case .calm: "Calm"
      case .content: "Content"
      case .disappointed: "Disappointed"
      case .discouraged: "Discouraged"
      case .disgusted: "Disgusted"
      case .embarrassed: "Embarrassed"
      case .excited: "Excited"
      case .frustrated: "Frustrated"
      case .grateful: "Grateful"
      case .guilty: "Guilty"
      case .happy: "Happy"
      case .hopeless: "Hopeless"
      case .irritated: "Irritated"
      case .jealous: "Jealous"
      case .joyful: "Joyful"
      case .lonely: "Lonely"
      case .passionate: "Passionate"
      case .peaceful: "Peaceful"
      case .proud: "Proud"
      case .relieved: "Relieved"
      case .sad: "Sad"
      case .scared: "Scared"
      case .stressed: "Stressed"
      case .surprised: "Surprised"
      case .worried: "Worried"
      case .annoyed: "Annoyed"
      case .confident: "Confident"
      case .drained: "Drained"
      case .hopeful: "Hopeful"
      case .indifferent: "Indifferent"
      case .overwhelmed: "Overwhelmed"
      case .satisfied: "Satisfied"
      @unknown default: "Unknown"
    }
  }

  static func fromIntegerValue(_ value: Int) -> HKStateOfMind.Label? {
    HKStateOfMind.Label(rawValue: value)
  }

  public static var allCases: [HKStateOfMind.Label] {
    [
      .amazed, .amused, .angry, .anxious, .ashamed,
      .brave, .calm, .content, .disappointed, .discouraged,
      .disgusted, .embarrassed, .excited, .frustrated, .grateful,
      .guilty, .happy, .hopeless, .irritated, .jealous,
      .joyful, .lonely, .passionate, .peaceful, .proud,
      .relieved, .sad, .scared, .stressed, .surprised,
      .worried, .annoyed, .confident, .drained, .hopeful,
      .indifferent, .overwhelmed, .satisfied
    ]
  }
}

Associations

Finally, the last enumeration is to describe the cause of the feeling, like partner, fitness, health, identity, etc.

Again, here is the description for each of them as an extension on HKStateOfMind.Association with allCases:

extension HKStateOfMind.Association: @retroactive CaseIterable, @retroactive CustomStringConvertible {
  var description: String {
    switch self {
      case .community: "Community"
      case .currentEvents: "Current Events"
      case .dating: "Dating"
      case .education: "Education"
      case .family: "Family"
      case .fitness: "Fitness"
      case .friends: "Friends"
      case .health: "Health"
      case .hobbies: "Hobbies"
      case .identity: "Identity"
      case .money: "Money"
      case .partner: "Partner"
      case .selfCare: "Self Care"
      case .spirituality: "Spirituality"
      case .tasks: "Tasks"
      case .travel: "Travel"
      case .work: "Work"
      case .weather: "Weather"
      @unknown default: "Unknown"
    }
  }
  
  public static var allCases: [HKStateOfMind.Association] {
    [
      .community, .currentEvents, .dating, .education, .family,
      .fitness, .friends, .health, .hobbies, .identity,
      .money, .partner, .selfCare, .spirituality, .tasks,
      .travel, .work, .weather
    ]
  }
}

HKStateOfMind

Now that we know the parameters and enumerations that is available for State of Mind, we can look into the initializer on how to create one:

public convenience init(
  date: Date,
  kind: HKStateOfMind.Kind,
  valence: Double,
  labels: [HKStateOfMind.Label],
  associations: [HKStateOfMind.Association],
  metadata: [String : Any]? = [:]
)

Let's break down each parameter:

  1. date: This is when you logged your state of mind. Usually, you'll use the current date and time.
  2. kind: Remember, this can be either .momentaryEmotion or .dailyMood.
  3. valence: This is how you are feeling on a scale from -1 (very unpleasant) to 1 (very pleasant).
  4. labels: These are the specific emotions you are feeling. You can choose one or more from the 38 available options.
  5. associations: These are what is causing your feelings. You can pick one or more from the 18 available options.
  6. metadata: This is optional extra information you might want to add. I still have not figured out how to use it, nor it is mentioned in the WWDC session.

Here's an example of how you might use this initializer:

let now = Date()
let stateOfMind = HKStateOfMind(
  date: now,
  kind: .momentaryEmotion,
  valence: 0.7,
  labels: [.happy, .excited],
  associations: [.work, .friends],
  metadata: ["notes": "Just finished a great project with my team!"]
)

Fetching State of Mind Data

Here is a method to fetch the mood data from the Health app that uses Swift concurrency:

func fetchStateOfMindData() async {
  do {
    let stateOfMindType = HKSampleType.stateOfMindType()
    let descriptor = HKSampleQueryDescriptor(
      predicates: [.sample(type: stateOfMindType)],
      sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)]
    )
    if let samples = try await descriptor.result(for: healthStore) as? [HKStateOfMind] {
      stateOfMindSamples = samples
    }
  } catch {
    debugPrint("Error fetching state of mind data: \(error.localizedDescription)")
  }
}

Creating a Mood Sample

Then, we have a method to create a mood sample:

func createSample() async {
  let now = Date()
  
  let sample = HKStateOfMind(
    date: now,
    kind: .momentaryEmotion,
    valence: 0.7,
    labels: [.happy, .excited],
    associations: [.work, .friends],
    metadata: ["notes": "Just finished a great project with my team!"] as [String : Any]
  )

  debugPrint("First sample: \(sample)")
  await save(sample: sample, healthStore: healthStore)
}

I am hoping I can use the metadata to add a note to the mood entry once I figure it out.

Saving State of Mind Data

Once we have created our sample, we need to save it:

func save(sample: HKSample, healthStore: HKHealthStore) async {
  do {
    try await healthStore.save(sample)
    print("State of Mind sample saved successfully")
    dismiss()
  } catch {
    print("Error saving State of Mind sample: \(error.localizedDescription)")
  }
}

Moving Forward

I am happy I got the APIs working smoothly with my Arising app. I want to figure out how to use that metadata field. Maybe I could use it to add voice notes or photos to mood entries if they allow that?

I also want to visualize all this data. A better calendar view where each day is color-coded based on your mood. And a graph that shows how your emotions fluctuate throughout the week. The main idea is to spot patterns. Maybe I am always happiest after a good workout, but that is a temporary happiness that wears down sooner than later.

A lot to figure out. Both for this app, and in life.

How do you plan to integrate the State of Mind APIs in your app? Drop me a line on X (@rudrankriyam) and let's chat about it!