·4 min read

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!

Post Topics

Explore more in these categories:

Related Articles

Exploring Stream's Video SDK: Creating a WWDC Watch Party App

Build a WWDC 2024 Watch Party App using Stream's Video SDK. Implement video playback, calling features, and synchronize playback for seamless group viewing. Dive into Stream's powerful tools to create interactive experiences for Apple developers and elevate your WWDC experience.

Exploring Claude: 3.7 Sonnet iOS Development

Explore the capabilities of Claude 3.7 Sonnet in iOS development, featuring improved code refactoring, SwiftUI integration, and efficient file handling. Learn how this AI model streamlines development workflows and handles complex coding tasks with remarkable accuracy and speed.