Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
b00b2cdafa
commit
30ef5f13ed
@ -12,6 +12,9 @@ final class ArcHabit {
|
||||
var goal: String = ""
|
||||
var completedDayIDs: [String] = []
|
||||
|
||||
/// Persisted sort order for deterministic ordering across CloudKit sync.
|
||||
var sortIndex: Int = 0
|
||||
|
||||
@Relationship(inverse: \RitualArc.habits)
|
||||
var arc: RitualArc?
|
||||
|
||||
@ -20,12 +23,14 @@ final class ArcHabit {
|
||||
title: String,
|
||||
symbolName: String,
|
||||
goal: String = "",
|
||||
sortIndex: Int = 0,
|
||||
completedDayIDs: [String] = []
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.symbolName = symbolName
|
||||
self.goal = goal
|
||||
self.sortIndex = sortIndex
|
||||
self.completedDayIDs = completedDayIDs
|
||||
}
|
||||
|
||||
@ -35,6 +40,7 @@ final class ArcHabit {
|
||||
title: title,
|
||||
symbolName: symbolName,
|
||||
goal: goal,
|
||||
sortIndex: sortIndex,
|
||||
completedDayIDs: []
|
||||
)
|
||||
}
|
||||
|
||||
@ -88,6 +88,10 @@ final class Ritual {
|
||||
var iconName: String = "sparkles"
|
||||
var category: String = ""
|
||||
|
||||
/// Persisted sort order for deterministic ordering across CloudKit sync.
|
||||
/// Used as secondary sort key after timeOfDay.
|
||||
var sortIndex: Int = 0
|
||||
|
||||
// Arcs - each arc represents a time-bound period with its own habits
|
||||
@Relationship(deleteRule: .cascade)
|
||||
var arcs: [RitualArc]? = []
|
||||
@ -101,6 +105,7 @@ final class Ritual {
|
||||
timeOfDay: TimeOfDay = .anytime,
|
||||
iconName: String = "sparkles",
|
||||
category: String = "",
|
||||
sortIndex: Int = 0,
|
||||
arcs: [RitualArc] = []
|
||||
) {
|
||||
self.id = id
|
||||
@ -111,6 +116,7 @@ final class Ritual {
|
||||
self.timeOfDay = timeOfDay
|
||||
self.iconName = iconName
|
||||
self.category = category
|
||||
self.sortIndex = sortIndex
|
||||
self.arcs = arcs
|
||||
}
|
||||
|
||||
@ -150,9 +156,9 @@ final class Ritual {
|
||||
|
||||
// MARK: - Convenience Accessors (for current arc)
|
||||
|
||||
/// Habits from the current arc (empty if no active arc).
|
||||
/// Habits from the current arc sorted by sortIndex (empty if no active arc).
|
||||
var habits: [ArcHabit] {
|
||||
currentArc?.habits ?? []
|
||||
(currentArc?.habits ?? []).sorted { $0.sortIndex < $1.sortIndex }
|
||||
}
|
||||
|
||||
/// Start date of the current arc.
|
||||
|
||||
@ -4,9 +4,9 @@ struct RitualSeedService: RitualSeedProviding {
|
||||
func makeSeedRituals(startDate: Date) -> [Ritual] {
|
||||
// Create morning ritual with arc
|
||||
let morningHabits = [
|
||||
ArcHabit(title: String(localized: "Hydrate"), symbolName: "drop.fill"),
|
||||
ArcHabit(title: String(localized: "Stretch"), symbolName: "figure.walk"),
|
||||
ArcHabit(title: String(localized: "Mindful minute"), symbolName: "sparkles")
|
||||
ArcHabit(title: String(localized: "Hydrate"), symbolName: "drop.fill", sortIndex: 0),
|
||||
ArcHabit(title: String(localized: "Stretch"), symbolName: "figure.walk", sortIndex: 1),
|
||||
ArcHabit(title: String(localized: "Mindful minute"), symbolName: "sparkles", sortIndex: 2)
|
||||
]
|
||||
let morningArc = RitualArc(
|
||||
startDate: startDate,
|
||||
@ -23,14 +23,15 @@ struct RitualSeedService: RitualSeedProviding {
|
||||
timeOfDay: .morning,
|
||||
iconName: "sunrise.fill",
|
||||
category: String(localized: "Wellness"),
|
||||
sortIndex: 0,
|
||||
arcs: [morningArc]
|
||||
)
|
||||
|
||||
// Create evening ritual with arc
|
||||
let eveningHabits = [
|
||||
ArcHabit(title: String(localized: "No screens"), symbolName: "moon.stars.fill"),
|
||||
ArcHabit(title: String(localized: "Read 10 pages"), symbolName: "book.fill"),
|
||||
ArcHabit(title: String(localized: "Reflect"), symbolName: "pencil.and.list.clipboard")
|
||||
ArcHabit(title: String(localized: "No screens"), symbolName: "moon.stars.fill", sortIndex: 0),
|
||||
ArcHabit(title: String(localized: "Read 10 pages"), symbolName: "book.fill", sortIndex: 1),
|
||||
ArcHabit(title: String(localized: "Reflect"), symbolName: "pencil.and.list.clipboard", sortIndex: 2)
|
||||
]
|
||||
let eveningStartDate = Calendar.current.date(byAdding: .day, value: -14, to: startDate) ?? startDate
|
||||
let eveningArc = RitualArc(
|
||||
@ -48,6 +49,7 @@ struct RitualSeedService: RitualSeedProviding {
|
||||
timeOfDay: .evening,
|
||||
iconName: "moon.stars.fill",
|
||||
category: String(localized: "Wellness"),
|
||||
sortIndex: 1,
|
||||
arcs: [eveningArc]
|
||||
)
|
||||
|
||||
|
||||
@ -609,9 +609,9 @@ final class RitualStore: RitualStoreProviding {
|
||||
func createQuickRitual() {
|
||||
let defaultDuration = 28
|
||||
let habits = [
|
||||
ArcHabit(title: String(localized: "Hydrate"), symbolName: "drop.fill"),
|
||||
ArcHabit(title: String(localized: "Move"), symbolName: "figure.walk"),
|
||||
ArcHabit(title: String(localized: "Reflect"), symbolName: "pencil.and.list.clipboard")
|
||||
ArcHabit(title: String(localized: "Hydrate"), symbolName: "drop.fill", sortIndex: 0),
|
||||
ArcHabit(title: String(localized: "Move"), symbolName: "figure.walk", sortIndex: 1),
|
||||
ArcHabit(title: String(localized: "Reflect"), symbolName: "pencil.and.list.clipboard", sortIndex: 2)
|
||||
]
|
||||
let arc = RitualArc(
|
||||
startDate: Date(),
|
||||
@ -620,11 +620,13 @@ final class RitualStore: RitualStoreProviding {
|
||||
isActive: true,
|
||||
habits: habits
|
||||
)
|
||||
let nextSortIndex = rituals.count
|
||||
let ritual = Ritual(
|
||||
title: String(localized: "Custom Ritual"),
|
||||
theme: String(localized: "Your next chapter"),
|
||||
defaultDurationDays: defaultDuration,
|
||||
notes: String(localized: "A fresh ritual created from your focus today."),
|
||||
sortIndex: nextSortIndex,
|
||||
arcs: [arc]
|
||||
)
|
||||
modelContext.insert(ritual)
|
||||
@ -642,13 +644,19 @@ final class RitualStore: RitualStoreProviding {
|
||||
category: String = "",
|
||||
habits: [ArcHabit] = []
|
||||
) {
|
||||
// Assign sortIndex to habits if not already set
|
||||
let indexedHabits = habits.enumerated().map { index, habit in
|
||||
habit.sortIndex = index
|
||||
return habit
|
||||
}
|
||||
let arc = RitualArc(
|
||||
startDate: Date(),
|
||||
durationDays: durationDays,
|
||||
arcNumber: 1,
|
||||
isActive: true,
|
||||
habits: habits
|
||||
habits: indexedHabits
|
||||
)
|
||||
let nextSortIndex = rituals.count
|
||||
let ritual = Ritual(
|
||||
title: title,
|
||||
theme: theme,
|
||||
@ -657,6 +665,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
timeOfDay: timeOfDay,
|
||||
iconName: iconName,
|
||||
category: category,
|
||||
sortIndex: nextSortIndex,
|
||||
arcs: [arc]
|
||||
)
|
||||
modelContext.insert(ritual)
|
||||
@ -665,8 +674,8 @@ final class RitualStore: RitualStoreProviding {
|
||||
|
||||
/// Creates a ritual from a preset template
|
||||
func createRitualFromPreset(_ preset: RitualPreset) {
|
||||
let habits = preset.habits.map { habitPreset in
|
||||
ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName)
|
||||
let habits = preset.habits.enumerated().map { index, habitPreset in
|
||||
ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName, sortIndex: index)
|
||||
}
|
||||
createRitual(
|
||||
title: preset.title,
|
||||
@ -684,8 +693,8 @@ final class RitualStore: RitualStoreProviding {
|
||||
/// Used during onboarding to immediately show the created ritual.
|
||||
@discardableResult
|
||||
func createRitual(from preset: RitualPreset) -> Ritual {
|
||||
let habits = preset.habits.map { habitPreset in
|
||||
ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName)
|
||||
let habits = preset.habits.enumerated().map { index, habitPreset in
|
||||
ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName, sortIndex: index)
|
||||
}
|
||||
let arc = RitualArc(
|
||||
startDate: Date(),
|
||||
@ -694,6 +703,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
isActive: true,
|
||||
habits: habits
|
||||
)
|
||||
let nextSortIndex = rituals.count
|
||||
let ritual = Ritual(
|
||||
title: preset.title,
|
||||
theme: preset.theme,
|
||||
@ -702,6 +712,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
timeOfDay: preset.timeOfDay,
|
||||
iconName: preset.iconName,
|
||||
category: preset.category,
|
||||
sortIndex: nextSortIndex,
|
||||
arcs: [arc]
|
||||
)
|
||||
modelContext.insert(ritual)
|
||||
@ -739,10 +750,12 @@ final class RitualStore: RitualStoreProviding {
|
||||
/// Adds a habit to the current arc of a ritual
|
||||
func addHabit(to ritual: Ritual, title: String, symbolName: String) {
|
||||
guard let arc = ritual.currentArc else { return }
|
||||
let habit = ArcHabit(title: title, symbolName: symbolName)
|
||||
var habits = arc.habits ?? []
|
||||
habits.append(habit)
|
||||
arc.habits = habits
|
||||
let habits = arc.habits ?? []
|
||||
let nextSortIndex = habits.map(\.sortIndex).max().map { $0 + 1 } ?? 0
|
||||
let habit = ArcHabit(title: title, symbolName: symbolName, sortIndex: nextSortIndex)
|
||||
var updatedHabits = habits
|
||||
updatedHabits.append(habit)
|
||||
arc.habits = updatedHabits
|
||||
saveContext()
|
||||
}
|
||||
|
||||
@ -785,7 +798,12 @@ final class RitualStore: RitualStoreProviding {
|
||||
private func updateDerivedData() {
|
||||
currentRituals = rituals
|
||||
.filter { $0.hasActiveArc }
|
||||
.sorted { $0.timeOfDay < $1.timeOfDay }
|
||||
.sorted { lhs, rhs in
|
||||
if lhs.timeOfDay != rhs.timeOfDay {
|
||||
return lhs.timeOfDay < rhs.timeOfDay
|
||||
}
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
pastRituals = rituals
|
||||
.filter { !$0.hasActiveArc }
|
||||
.sorted { ($0.lastCompletedDate ?? .distantPast) > ($1.lastCompletedDate ?? .distantPast) }
|
||||
|
||||
@ -442,7 +442,9 @@ struct RitualEditSheet: View {
|
||||
}
|
||||
} else {
|
||||
// Create new ritual
|
||||
let newHabits = habits.map { ArcHabit(title: $0.title, symbolName: $0.symbolName) }
|
||||
let newHabits = habits.enumerated().map { index, habit in
|
||||
ArcHabit(title: habit.title, symbolName: habit.symbolName, sortIndex: index)
|
||||
}
|
||||
store.createRitual(
|
||||
title: trimmedTitle,
|
||||
theme: trimmedTheme,
|
||||
|
||||
@ -5,17 +5,20 @@ struct TodayHabitRowView: View {
|
||||
private let title: String
|
||||
private let symbolName: String
|
||||
private let isCompleted: Bool
|
||||
private let horizontalPadding: CGFloat
|
||||
private let action: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
symbolName: String,
|
||||
isCompleted: Bool,
|
||||
horizontalPadding: CGFloat = Design.Spacing.large,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.symbolName = symbolName
|
||||
self.isCompleted = isCompleted
|
||||
self.horizontalPadding = horizontalPadding
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -39,7 +42,7 @@ struct TodayHabitRowView: View {
|
||||
.foregroundStyle(isCompleted ? AppStatus.success : AppBorder.subtle)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.background(AppSurface.card)
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
||||
|
||||
@ -75,6 +75,7 @@ struct TodayRitualSectionView: View {
|
||||
title: habit.title,
|
||||
symbolName: habit.symbolName,
|
||||
isCompleted: habit.isCompleted,
|
||||
horizontalPadding: 0,
|
||||
action: habit.action
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user