Exploring Journaling Suggestions: Adding Reflection Prompt

I am working on a journaling app for myself, Harmony, and started implementing Journaling Suggestions framework into it. I want to add more depth to it and this year, Apple made the Reflection prompt available to developers outside of their own Journal app.

Reflection Prompt

What is a Reflection prompt? It is a suggestion that encourages users to pause and think deeply about a particular topic or question. I can filter through the prompts given by the system, and reflect upon them according to the moment.

Understanding the JournalingSuggestion.Reflection Structure

The JournalingSuggestion.Reflection structure, available in iOS 18.0+ (currently in beta), represents this prompt in code:

/// A suggestion for a reflection prompt.
///
/// The system provides an instance of this structure to your app when a person chooses a reflection prompt suggestion
///  in the ``JournalingSuggestionsPicker``.
@available(iOS 18.0, *)
public struct Reflection : JournalingSuggestionAsset { }

This structure has two key properties:

  1. color: An optional Color that sets the background for the prompt in the picker.
  2. prompt: A String containing the actual reflection question or statement.

The system provides an array of this structure when the user selects a reflection prompt from the JournalingSuggestionsPicker.

Implementing Reflection Prompts

Here is a simple implementation for getting started with the Reflection prompts. We start by importing the required modules:

import JournalingSuggestions
import SwiftUI

The ReflectionView is our main view:

@available(iOS 18.0, *)
struct ReflectionView: View {
  @State private var reflectionSuggestions: [JournalingSuggestion.Reflection] = []
  @State private var suggestionTitle: String?
  
  var body: some View {
    NavigationStack {
      VStack {
        ForEach($reflectionSuggestions, content: ReflectionPromptView.init)
        
        JournalingSuggestionsPicker {
          Text("Get Reflection.")
        } onCompletion: { suggestion in
          suggestionTitle = suggestion.title
          reflectionSuggestions = await suggestion.content(forType: JournalingSuggestion.Reflection.self)
        }
        .buttonStyle(.borderedProminent)
      }
      .navigationTitle(suggestionTitle ?? "")
    }
  }
}

This view includes a JournalingSuggestionsPicker to fetch new prompts and a ForEach loop to display existing prompts using the ReflectionPromptView.

To use Reflection in SwiftUI's ForEach, we need to make it conform to the Identifiable protocol:

@available(iOS 18.0, *)
extension JournalingSuggestion.Reflection: @retroactive Identifiable {
  public var id: String {
    self.prompt
  }
}

The ReflectionPromptView struct is responsible for displaying individual reflection prompts:

@available(iOS 18.0, *)
struct ReflectionPromptView: View {
  @Binding var suggestion: JournalingSuggestion.Reflection

  var body: some View {
    let reflectionSuggestion = $suggestion.wrappedValue
    let backgroundColor = reflectionSuggestion.color ?? .white

    Text(reflectionSuggestion.prompt)
      .font(.title)
      .bold()
      .multilineTextAlignment(.center)
      .foregroundStyle(foregroundColor(for: backgroundColor))
      .frame(maxHeight: .infinity)
      .padding()
      .background(backgroundColor, in: RoundedRectangle(cornerRadius: 16))
      .padding()
  }

  private func foregroundColor(for backgroundColor: Color) -> Color {
    var whiteValue = CGFloat()
    if UIColor(backgroundColor).getWhite(&whiteValue, alpha: nil) {
      return whiteValue > 0.6 ? .black : .white
    }
    return .white
  }
}

This view handles the display of each prompt, including text styling and background color. The foregroundColor function ensures the text is always readable against the background.

Here's the complete code for implementing Reflection prompts:

@available(iOS 18.0, *)
struct ReflectionView: View {
  @State private var reflectionSuggestions: [JournalingSuggestion.Reflection] = []
  @State private var suggestionTitle: String?

  var body: some View {
    NavigationStack {
      VStack {
        ForEach($reflectionSuggestions, content: ReflectionPromptView.init)

        JournalingSuggestionsPicker {
          Text("Get Reflection.")
        } onCompletion: { suggestion in
          suggestionTitle = suggestion.title
          reflectionSuggestions = await suggestion.content(forType: JournalingSuggestion.Reflection.self)
        }
        .buttonStyle(.borderedProminent)
      }
      .navigationTitle(suggestionTitle ?? "")
    }
  }
}

@available(iOS 18.0, *)
extension JournalingSuggestion.Reflection: @retroactive Identifiable {
  public var id: String {
    self.prompt
  }
}

@available(iOS 18.0, *)
struct ReflectionPromptView: View {
  @Binding var suggestion: JournalingSuggestion.Reflection

  var body: some View {
    let reflectionSuggestion = $suggestion.wrappedValue
    let backgroundColor = reflectionSuggestion.color ?? .white

    Text(reflectionSuggestion.prompt)
      .font(.title)
      .bold()
      .multilineTextAlignment(.center)
      .foregroundStyle(foregroundColor(for: backgroundColor))
      .frame(maxHeight: .infinity)
      .padding()
      .background(backgroundColor, in: RoundedRectangle(cornerRadius: 16))
      .padding()
  }

  private func foregroundColor(for backgroundColor: Color) -> Color {
    var whiteValue = CGFloat()
    if UIColor(backgroundColor).getWhite(&whiteValue, alpha: nil) {
      return whiteValue > 0.6 ? .black : .white
    }
    return .white
  }
}

How It Works

When the user taps Get Reflection., the system presents a picker interface like the one I have in the image shown before. Once a prompt is selected, the onCompletion closure is called with a JournalingSuggestion object. This object contains several pieces of information, including the actual reflection prompt.

Here's what the raw JournalingSuggestion object looks like when printed to the console:

JournalingSuggestion(items: [JournalingSuggestions.JournalingSuggestion.ItemContent(id: 6B18D50A-AA8F-4BEB-814D-182D3E5EE6BA, representations: [JournalingSuggestions.JournalingSuggestion.Reflection], content: JournalingSuggestions.InternalAssetContent(providers: [JournalingSuggestions.InternalAssetContent.AssetProvider(type: JournalingSuggestions.JournalingSuggestion.Reflection, loader: (Function))]))], title: "Gratitude Reflection", date: Optional(2024-07-07 04:59:47 +0000 to 2024-07-07 05:59:47 +0000), suggestionIdentifier: F3B97E7C-6304-46F1-A0DB-9DD58BF597B8, suggestionHashValue: 12949094976)

The important part is the ItemContent, which contains the Reflection prompt. I access this with:

reflectionSuggestions = await suggestion.content(forType: JournalingSuggestion.Reflection.self)

Running the app:

Conclusion

I am excited about the possibilities Reflection prompts bring to journaling apps. I have been thinking about how I can use LLM APIs to reflect further, but that's for another day.

Remember, this feature is still in beta for iOS 18.0. Keep an eye on Apple's documentation for any changes or updates as we approach the official release in September. Happy coding and journaling!