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