Negative Q4 (Reframe): Moved cognitive reframes to top row (challenge worst-case, separate facts from feelings, etc.) and demoted action chips (take a walk, get rest) to expanded. Added two new reframe chips. Positive Q2 (Awareness): Replaced single emotion words (Joy, Gratitude) with moment-oriented suggestions (A conversation that made me smile, Something I accomplished) to match "what moment stands out?" question. Added translations for 14 new localization keys across all 7 languages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
348 lines
14 KiB
Swift
348 lines
14 KiB
Swift
//
|
|
// GuidedReflection.swift
|
|
// Reflect
|
|
//
|
|
// Codable model for guided reflection responses, stored as JSON in MoodEntryModel.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Mood Category
|
|
|
|
enum MoodCategory: String, Codable {
|
|
case positive // great, good → 3 questions (Behavioral Activation)
|
|
case neutral // average → 4 questions (ACT Cognitive Defusion)
|
|
case negative // bad, horrible → 4 questions (CBT Thought Record)
|
|
|
|
init(from mood: Mood) {
|
|
switch mood {
|
|
case .great, .good: self = .positive
|
|
case .average: self = .neutral
|
|
case .horrible, .bad: self = .negative
|
|
default: self = .neutral
|
|
}
|
|
}
|
|
|
|
var questionCount: Int {
|
|
switch self {
|
|
case .positive: return 3
|
|
case .neutral, .negative: return 4
|
|
}
|
|
}
|
|
|
|
/// The therapeutic technique name for display purposes.
|
|
var techniqueName: String {
|
|
switch self {
|
|
case .positive: return "Behavioral Activation"
|
|
case .neutral: return "Acceptance & Commitment Therapy"
|
|
case .negative: return "Cognitive Behavioral Therapy"
|
|
}
|
|
}
|
|
|
|
/// Short CBT step labels shown above each question.
|
|
var stepLabels: [String] {
|
|
switch self {
|
|
case .negative:
|
|
return [
|
|
String(localized: "Situation"),
|
|
String(localized: "Automatic Thought"),
|
|
String(localized: "Perspective Check"),
|
|
String(localized: "Reframe"),
|
|
]
|
|
case .neutral:
|
|
return [
|
|
String(localized: "Awareness"),
|
|
String(localized: "Thought"),
|
|
String(localized: "Defusion"),
|
|
String(localized: "Values"),
|
|
]
|
|
case .positive:
|
|
return [
|
|
String(localized: "Activity"),
|
|
String(localized: "Awareness"),
|
|
String(localized: "Planning"),
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Question Chips
|
|
|
|
struct QuestionChips {
|
|
let topRow: [String]
|
|
let expanded: [String]
|
|
|
|
var hasExpanded: Bool { !expanded.isEmpty }
|
|
|
|
static func chips(for category: MoodCategory, questionIndex: Int) -> QuestionChips? {
|
|
switch (category, questionIndex) {
|
|
|
|
// MARK: Positive (Great/Good) — Behavioral Activation
|
|
|
|
// Q1: "What did you do today…?" — no chips (situational)
|
|
// Q2: "What thought or moment stands out?" — memorable moments to savor
|
|
case (.positive, 1):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_pos_moment_conversation"),
|
|
String(localized: "guided_chip_pos_moment_accomplished"),
|
|
String(localized: "guided_chip_pos_moment_calm"),
|
|
String(localized: "guided_chip_pos_moment_laugh"),
|
|
String(localized: "guided_chip_pos_moment_grateful_person"),
|
|
String(localized: "guided_chip_pos_moment_small_win"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_pos_moment_beauty"),
|
|
String(localized: "guided_chip_pos_moment_connected"),
|
|
String(localized: "guided_chip_pos_moment_progress"),
|
|
String(localized: "guided_chip_pos_moment_like_myself"),
|
|
String(localized: "guided_chip_pos_moment_kindness"),
|
|
String(localized: "guided_chip_pos_moment_time_well_spent"),
|
|
]
|
|
)
|
|
|
|
// Q3: "How could you create more days like this?" — reinforcing actions
|
|
case (.positive, 2):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_pos_act_more_of_this"),
|
|
String(localized: "guided_chip_pos_act_time_with_people"),
|
|
String(localized: "guided_chip_pos_act_get_outside"),
|
|
String(localized: "guided_chip_pos_act_stay_active"),
|
|
String(localized: "guided_chip_pos_act_keep_routine"),
|
|
String(localized: "guided_chip_pos_act_practice_gratitude"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_pos_act_celebrate_wins"),
|
|
String(localized: "guided_chip_pos_act_hobbies"),
|
|
String(localized: "guided_chip_pos_act_help_someone"),
|
|
String(localized: "guided_chip_pos_act_say_yes"),
|
|
String(localized: "guided_chip_pos_act_rest"),
|
|
String(localized: "guided_chip_pos_act_limit_doomscroll"),
|
|
]
|
|
)
|
|
|
|
// MARK: Neutral (Average) — ACT Cognitive Defusion
|
|
|
|
// Q1: "What feeling has been sitting with you?" — ambivalent feelings
|
|
case (.neutral, 0):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neu_boredom"),
|
|
String(localized: "guided_chip_neu_restlessness"),
|
|
String(localized: "guided_chip_neu_uncertainty"),
|
|
String(localized: "guided_chip_neu_numbness"),
|
|
String(localized: "guided_chip_neu_indifference"),
|
|
String(localized: "guided_chip_neu_distraction"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_neu_flatness"),
|
|
String(localized: "guided_chip_neu_disconnection"),
|
|
String(localized: "guided_chip_neu_ambivalence"),
|
|
String(localized: "guided_chip_neu_weariness"),
|
|
String(localized: "guided_chip_neu_apathy"),
|
|
String(localized: "guided_chip_neu_autopilot"),
|
|
]
|
|
)
|
|
|
|
// Q2: "What thought is connected to that feeling?" — no chips (specific thought)
|
|
// Q3: "Is that thought true, or something your mind is telling you?" — defusion reframes
|
|
case (.neutral, 2):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neu_def_just_a_thought"),
|
|
String(localized: "guided_chip_neu_def_storytelling"),
|
|
String(localized: "guided_chip_neu_def_thought_passed"),
|
|
String(localized: "guided_chip_neu_def_more_weight"),
|
|
String(localized: "guided_chip_neu_def_not_helpful"),
|
|
String(localized: "guided_chip_neu_def_notice_not_act"),
|
|
String(localized: "guided_chip_neu_def_dont_solve_now"),
|
|
String(localized: "guided_chip_neu_def_doesnt_define"),
|
|
],
|
|
expanded: []
|
|
)
|
|
|
|
// Q4: "What matters to you about tomorrow?" — values-aligned intentions
|
|
case (.neutral, 3):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neu_val_be_present"),
|
|
String(localized: "guided_chip_neu_val_connect"),
|
|
String(localized: "guided_chip_neu_val_body"),
|
|
String(localized: "guided_chip_neu_val_meaningful_work"),
|
|
String(localized: "guided_chip_neu_val_kind_to_self"),
|
|
String(localized: "guided_chip_neu_val_small_goal"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_neu_val_less_autopilot"),
|
|
String(localized: "guided_chip_neu_val_one_thing_better"),
|
|
String(localized: "guided_chip_neu_val_ask_what_i_need"),
|
|
String(localized: "guided_chip_neu_val_let_go"),
|
|
]
|
|
)
|
|
|
|
// MARK: Negative (Bad/Horrible) — CBT Thought Record
|
|
|
|
// Q1: "What happened today…?" — no chips (situational)
|
|
// Q2: "What thought kept coming back?" — common automatic negative thoughts
|
|
case (.negative, 1):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neg_not_good_enough"),
|
|
String(localized: "guided_chip_neg_nothing_goes_right"),
|
|
String(localized: "guided_chip_neg_all_my_fault"),
|
|
String(localized: "guided_chip_neg_cant_handle"),
|
|
String(localized: "guided_chip_neg_no_one_understands"),
|
|
String(localized: "guided_chip_neg_should_have"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_neg_never_change"),
|
|
String(localized: "guided_chip_neg_falling_behind"),
|
|
String(localized: "guided_chip_neg_dont_matter"),
|
|
String(localized: "guided_chip_neg_something_wrong"),
|
|
String(localized: "guided_chip_neg_letting_down"),
|
|
String(localized: "guided_chip_neg_cant_do_right"),
|
|
]
|
|
)
|
|
|
|
// Q3: "What would you tell a friend?" — compassionate reframes
|
|
case (.negative, 2):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neg_too_hard"),
|
|
String(localized: "guided_chip_neg_one_bad_day"),
|
|
String(localized: "guided_chip_neg_better_than_think"),
|
|
String(localized: "guided_chip_neg_ok_to_struggle"),
|
|
String(localized: "guided_chip_neg_feeling_will_pass"),
|
|
String(localized: "guided_chip_neg_dont_need_figured"),
|
|
String(localized: "guided_chip_neg_not_whole_story"),
|
|
String(localized: "guided_chip_neg_give_grace"),
|
|
],
|
|
expanded: []
|
|
)
|
|
|
|
// Q4: "More balanced way to see it?" — cognitive reframes first, grounding actions expanded
|
|
case (.negative, 3):
|
|
return QuestionChips(
|
|
topRow: [
|
|
String(localized: "guided_chip_neg_act_worst_case"),
|
|
String(localized: "guided_chip_neg_act_facts_feelings"),
|
|
String(localized: "guided_chip_neg_act_matter_in_week"),
|
|
String(localized: "guided_chip_neg_act_got_through"),
|
|
String(localized: "guided_chip_neg_ref_one_chapter"),
|
|
String(localized: "guided_chip_neg_ref_doing_my_best"),
|
|
],
|
|
expanded: [
|
|
String(localized: "guided_chip_neg_act_talk_someone"),
|
|
String(localized: "guided_chip_neg_act_write_it_out"),
|
|
String(localized: "guided_chip_neg_act_take_walk"),
|
|
String(localized: "guided_chip_neg_act_step_away"),
|
|
String(localized: "guided_chip_neg_act_get_rest"),
|
|
String(localized: "guided_chip_neg_act_one_small_thing"),
|
|
]
|
|
)
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Guided Reflection
|
|
|
|
struct GuidedReflection: Codable, Equatable {
|
|
|
|
struct Response: Codable, Equatable, Identifiable {
|
|
var id: Int // question index (0-based)
|
|
let question: String
|
|
var answer: String
|
|
var selectedChips: [String] = []
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, question, answer, selectedChips
|
|
}
|
|
|
|
init(id: Int, question: String, answer: String, selectedChips: [String] = []) {
|
|
self.id = id
|
|
self.question = question
|
|
self.answer = answer
|
|
self.selectedChips = selectedChips
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decode(Int.self, forKey: .id)
|
|
question = try container.decode(String.self, forKey: .question)
|
|
answer = try container.decode(String.self, forKey: .answer)
|
|
selectedChips = try container.decodeIfPresent([String].self, forKey: .selectedChips) ?? []
|
|
}
|
|
}
|
|
|
|
let moodCategory: MoodCategory
|
|
var responses: [Response]
|
|
var completedAt: Date?
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
var isComplete: Bool {
|
|
responses.count == moodCategory.questionCount &&
|
|
responses.allSatisfy { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
|
}
|
|
|
|
var answeredCount: Int {
|
|
responses.filter { !$0.answer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
|
|
}
|
|
|
|
var totalQuestions: Int {
|
|
moodCategory.questionCount
|
|
}
|
|
|
|
// MARK: - Factory
|
|
|
|
static func createNew(for mood: Mood) -> GuidedReflection {
|
|
let category = MoodCategory(from: mood)
|
|
let questionTexts = questions(for: category)
|
|
let responses = questionTexts.enumerated().map { index, question in
|
|
Response(id: index, question: question, answer: "")
|
|
}
|
|
return GuidedReflection(moodCategory: category, responses: responses, completedAt: nil)
|
|
}
|
|
|
|
static func questions(for category: MoodCategory) -> [String] {
|
|
switch category {
|
|
case .positive:
|
|
return [
|
|
String(localized: "guided_reflection_positive_q1"),
|
|
String(localized: "guided_reflection_positive_q2"),
|
|
String(localized: "guided_reflection_positive_q3"),
|
|
]
|
|
case .neutral:
|
|
return [
|
|
String(localized: "guided_reflection_neutral_q1"),
|
|
String(localized: "guided_reflection_neutral_q2"),
|
|
String(localized: "guided_reflection_neutral_q3"),
|
|
String(localized: "guided_reflection_neutral_q4"),
|
|
]
|
|
case .negative:
|
|
return [
|
|
String(localized: "guided_reflection_negative_q1"),
|
|
String(localized: "guided_reflection_negative_q2"),
|
|
String(localized: "guided_reflection_negative_q3"),
|
|
String(localized: "guided_reflection_negative_q4"),
|
|
]
|
|
}
|
|
}
|
|
|
|
// MARK: - JSON Helpers
|
|
|
|
func encode() -> String? {
|
|
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
static func decode(from json: String) -> GuidedReflection? {
|
|
guard let data = json.data(using: .utf8) else { return nil }
|
|
return try? JSONDecoder().decode(GuidedReflection.self, from: data)
|
|
}
|
|
}
|