Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2eb2abfba8
commit
dd89905b29
@ -3,7 +3,9 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<string>iCloud.com.mbrucedogs.Andromida</string>
|
||||||
|
</array>
|
||||||
<key>com.apple.developer.icloud-services</key>
|
<key>com.apple.developer.icloud-services</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CloudKit</string>
|
<string>CloudKit</string>
|
||||||
|
|||||||
@ -16,7 +16,11 @@ struct AndromidaApp: App {
|
|||||||
init() {
|
init() {
|
||||||
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
||||||
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
||||||
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
let configuration = ModelConfiguration(
|
||||||
|
schema: schema,
|
||||||
|
isStoredInMemoryOnly: false,
|
||||||
|
cloudKitDatabase: .private("iCloud.com.mbrucedogs.Andromida")
|
||||||
|
)
|
||||||
let container: ModelContainer
|
let container: ModelContainer
|
||||||
do {
|
do {
|
||||||
container = try ModelContainer(for: schema, configurations: [configuration])
|
container = try ModelContainer(for: schema, configurations: [configuration])
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import SwiftData
|
|||||||
/// while preserving historical data.
|
/// while preserving historical data.
|
||||||
@Model
|
@Model
|
||||||
final class ArcHabit {
|
final class ArcHabit {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var title: String
|
var title: String = ""
|
||||||
var symbolName: String
|
var symbolName: String = ""
|
||||||
var goal: String
|
var goal: String = ""
|
||||||
var completedDayIDs: [String]
|
var completedDayIDs: [String] = []
|
||||||
|
|
||||||
@Relationship(inverse: \RitualArc.habits)
|
@Relationship(inverse: \RitualArc.habits)
|
||||||
var arc: RitualArc?
|
var arc: RitualArc?
|
||||||
|
|||||||
@ -73,24 +73,24 @@ enum TimeOfDay: String, Codable, CaseIterable, Comparable {
|
|||||||
/// This allows rituals to be renewed while preserving historical accuracy.
|
/// This allows rituals to be renewed while preserving historical accuracy.
|
||||||
@Model
|
@Model
|
||||||
final class Ritual {
|
final class Ritual {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var title: String
|
var title: String = ""
|
||||||
var theme: String
|
var theme: String = ""
|
||||||
var notes: String
|
var notes: String = ""
|
||||||
|
|
||||||
// Default duration for new arcs
|
// Default duration for new arcs
|
||||||
var defaultDurationDays: Int
|
var defaultDurationDays: Int = 28
|
||||||
|
|
||||||
// Scheduling
|
// Scheduling
|
||||||
var timeOfDay: TimeOfDay
|
var timeOfDay: TimeOfDay = .anytime
|
||||||
|
|
||||||
// Organization
|
// Organization
|
||||||
var iconName: String
|
var iconName: String = "sparkles"
|
||||||
var category: String
|
var category: String = ""
|
||||||
|
|
||||||
// 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]? = []
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: UUID = UUID(),
|
id: UUID = UUID(),
|
||||||
@ -118,7 +118,7 @@ final class Ritual {
|
|||||||
|
|
||||||
/// The currently active arc, if any.
|
/// The currently active arc, if any.
|
||||||
var currentArc: RitualArc? {
|
var currentArc: RitualArc? {
|
||||||
arcs.first { $0.isActive }
|
arcs?.first { $0.isActive }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this ritual has an active arc in progress.
|
/// Whether this ritual has an active arc in progress.
|
||||||
@ -128,7 +128,7 @@ final class Ritual {
|
|||||||
|
|
||||||
/// All arcs sorted by start date (newest first).
|
/// All arcs sorted by start date (newest first).
|
||||||
var sortedArcs: [RitualArc] {
|
var sortedArcs: [RitualArc] {
|
||||||
arcs.sorted { $0.startDate > $1.startDate }
|
(arcs ?? []).sorted { $0.startDate > $1.startDate }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The most recent arc (active or completed).
|
/// The most recent arc (active or completed).
|
||||||
@ -138,12 +138,12 @@ final class Ritual {
|
|||||||
|
|
||||||
/// Total number of completed arcs.
|
/// Total number of completed arcs.
|
||||||
var completedArcCount: Int {
|
var completedArcCount: Int {
|
||||||
arcs.filter { !$0.isActive }.count
|
(arcs ?? []).filter { !$0.isActive }.count
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The end date of the most recently completed arc, if any.
|
/// The end date of the most recently completed arc, if any.
|
||||||
var lastCompletedDate: Date? {
|
var lastCompletedDate: Date? {
|
||||||
arcs.filter { !$0.isActive }
|
(arcs ?? []).filter { !$0.isActive }
|
||||||
.sorted { $0.endDate > $1.endDate }
|
.sorted { $0.endDate > $1.endDate }
|
||||||
.first?.endDate
|
.first?.endDate
|
||||||
}
|
}
|
||||||
@ -174,12 +174,12 @@ final class Ritual {
|
|||||||
|
|
||||||
/// Returns the arc that was active on a specific date, if any.
|
/// Returns the arc that was active on a specific date, if any.
|
||||||
func arc(for date: Date) -> RitualArc? {
|
func arc(for date: Date) -> RitualArc? {
|
||||||
arcs.first { $0.contains(date: date) }
|
(arcs ?? []).first { $0.contains(date: date) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all arcs that overlap with a date range.
|
/// Returns all arcs that overlap with a date range.
|
||||||
func arcs(in range: ClosedRange<Date>) -> [RitualArc] {
|
func arcs(in range: ClosedRange<Date>) -> [RitualArc] {
|
||||||
arcs.filter { arc in
|
(arcs ?? []).filter { arc in
|
||||||
// Arc overlaps if its range intersects with the query range
|
// Arc overlaps if its range intersects with the query range
|
||||||
arc.endDate >= range.lowerBound && arc.startDate <= range.upperBound
|
arc.endDate >= range.lowerBound && arc.startDate <= range.upperBound
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import SwiftData
|
|||||||
/// the old arc's data remains frozen for historical accuracy.
|
/// the old arc's data remains frozen for historical accuracy.
|
||||||
@Model
|
@Model
|
||||||
final class RitualArc {
|
final class RitualArc {
|
||||||
var id: UUID
|
var id: UUID = UUID()
|
||||||
var startDate: Date
|
var startDate: Date = Date()
|
||||||
var endDate: Date
|
var endDate: Date = Date()
|
||||||
var arcNumber: Int
|
var arcNumber: Int = 1
|
||||||
var isActive: Bool
|
var isActive: Bool = true
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade)
|
@Relationship(deleteRule: .cascade)
|
||||||
var habits: [ArcHabit]
|
var habits: [ArcHabit]? = []
|
||||||
|
|
||||||
@Relationship(inverse: \Ritual.arcs)
|
@Relationship(inverse: \Ritual.arcs)
|
||||||
var ritual: Ritual?
|
var ritual: Ritual?
|
||||||
@ -88,7 +88,7 @@ final class RitualArc {
|
|||||||
let newStartDate = calendar.date(byAdding: .day, value: 1, to: endDate) ?? Date()
|
let newStartDate = calendar.date(byAdding: .day, value: 1, to: endDate) ?? Date()
|
||||||
let newDuration = durationDays ?? self.durationDays
|
let newDuration = durationDays ?? self.durationDays
|
||||||
|
|
||||||
let copiedHabits = habits.map { $0.copyForNewArc() }
|
let copiedHabits = (habits ?? []).map { $0.copyForNewArc() }
|
||||||
|
|
||||||
return RitualArc(
|
return RitualArc(
|
||||||
startDate: newStartDate,
|
startDate: newStartDate,
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import os
|
|
||||||
|
|
||||||
enum PerformanceLogger {
|
|
||||||
private static let logger = Logger(
|
|
||||||
subsystem: Bundle.main.bundleIdentifier ?? "Andromida",
|
|
||||||
category: "Performance"
|
|
||||||
)
|
|
||||||
|
|
||||||
static func measure<T>(_ name: String, _ block: () -> T) -> T {
|
|
||||||
#if DEBUG
|
|
||||||
let start = CFAbsoluteTimeGetCurrent()
|
|
||||||
let result = block()
|
|
||||||
let duration = CFAbsoluteTimeGetCurrent() - start
|
|
||||||
logger.info("\(name, privacy: .public) took \(duration, format: .fixed(precision: 3))s")
|
|
||||||
return result
|
|
||||||
#else
|
|
||||||
return block()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static func logDuration(_ name: String, from start: CFAbsoluteTime) {
|
|
||||||
#if DEBUG
|
|
||||||
let duration = CFAbsoluteTimeGetCurrent() - start
|
|
||||||
logger.info("\(name, privacy: .public) took \(duration, format: .fixed(precision: 3))s")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -72,11 +72,9 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
/// Refreshes rituals and derived state for current date/time.
|
/// Refreshes rituals and derived state for current date/time.
|
||||||
func refresh() {
|
func refresh() {
|
||||||
PerformanceLogger.measure("RitualStore.refresh") {
|
|
||||||
reloadRituals()
|
reloadRituals()
|
||||||
checkForCompletedArcs()
|
checkForCompletedArcs()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func ritualProgress(for ritual: Ritual) -> Double {
|
func ritualProgress(for ritual: Ritual) -> Double {
|
||||||
let habits = ritual.habits
|
let habits = ritual.habits
|
||||||
@ -174,12 +172,12 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
/// Returns all arcs that were active on a specific date.
|
/// Returns all arcs that were active on a specific date.
|
||||||
func arcsActive(on date: Date) -> [RitualArc] {
|
func arcsActive(on date: Date) -> [RitualArc] {
|
||||||
rituals.flatMap { $0.arcs }.filter { $0.contains(date: date) }
|
rituals.flatMap { $0.arcs ?? [] }.filter { $0.contains(date: date) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns habits from all arcs that were active on a specific date.
|
/// Returns habits from all arcs that were active on a specific date.
|
||||||
func habitsActive(on date: Date) -> [ArcHabit] {
|
func habitsActive(on date: Date) -> [ArcHabit] {
|
||||||
arcsActive(on: date).flatMap { $0.habits }
|
arcsActive(on: date).flatMap { $0.habits ?? [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a ritual's current arc has completed (past end date).
|
/// Checks if a ritual's current arc has completed (past end date).
|
||||||
@ -217,7 +215,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
let newHabits: [ArcHabit]
|
let newHabits: [ArcHabit]
|
||||||
if copyHabits, let previousArc = ritual.latestArc {
|
if copyHabits, let previousArc = ritual.latestArc {
|
||||||
newHabits = previousArc.habits.map { $0.copyForNewArc() }
|
newHabits = (previousArc.habits ?? []).map { $0.copyForNewArc() }
|
||||||
} else {
|
} else {
|
||||||
newHabits = []
|
newHabits = []
|
||||||
}
|
}
|
||||||
@ -230,7 +228,9 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
habits: newHabits
|
habits: newHabits
|
||||||
)
|
)
|
||||||
|
|
||||||
ritual.arcs.append(newArc)
|
var arcs = ritual.arcs ?? []
|
||||||
|
arcs.append(newArc)
|
||||||
|
ritual.arcs = arcs
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +360,10 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
var breakdown: [BreakdownItem] = []
|
var breakdown: [BreakdownItem] = []
|
||||||
|
|
||||||
// Total check-ins
|
// Total check-ins
|
||||||
let totalCheckIns = rituals.flatMap { $0.arcs }.flatMap { $0.habits }.reduce(0) { $0 + $1.completedDayIDs.count }
|
let totalCheckIns = rituals
|
||||||
|
.flatMap { $0.arcs ?? [] }
|
||||||
|
.flatMap { $0.habits ?? [] }
|
||||||
|
.reduce(0) { $0 + $1.completedDayIDs.count }
|
||||||
breakdown.append(BreakdownItem(
|
breakdown.append(BreakdownItem(
|
||||||
label: String(localized: "Total check-ins"),
|
label: String(localized: "Total check-ins"),
|
||||||
value: "\(totalCheckIns)"
|
value: "\(totalCheckIns)"
|
||||||
@ -392,7 +395,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
// Per-ritual breakdown
|
// Per-ritual breakdown
|
||||||
for ritual in rituals {
|
for ritual in rituals {
|
||||||
let ritualDays = Set(ritual.arcs.flatMap { $0.habits }.flatMap { $0.completedDayIDs }).count
|
let ritualDays = Set((ritual.arcs ?? []).flatMap { $0.habits ?? [] }.flatMap { $0.completedDayIDs }).count
|
||||||
breakdown.append(BreakdownItem(
|
breakdown.append(BreakdownItem(
|
||||||
label: ritual.title,
|
label: ritual.title,
|
||||||
value: String(localized: "\(ritualDays) days")
|
value: String(localized: "\(ritualDays) days")
|
||||||
@ -409,9 +412,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
func refreshInsightCardsIfNeeded() {
|
func refreshInsightCardsIfNeeded() {
|
||||||
guard insightCardsNeedRefresh else { return }
|
guard insightCardsNeedRefresh else { return }
|
||||||
cachedInsightCards = PerformanceLogger.measure("RitualStore.insightCards") {
|
cachedInsightCards = computeInsightCards()
|
||||||
computeInsightCards()
|
|
||||||
}
|
|
||||||
insightCardsNeedRefresh = false
|
insightCardsNeedRefresh = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,14 +631,18 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
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 habit = ArcHabit(title: title, symbolName: symbolName)
|
||||||
arc.habits.append(habit)
|
var habits = arc.habits ?? []
|
||||||
|
habits.append(habit)
|
||||||
|
arc.habits = habits
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a habit from the current arc of a ritual
|
/// Removes a habit from the current arc of a ritual
|
||||||
func removeHabit(_ habit: ArcHabit, from ritual: Ritual) {
|
func removeHabit(_ habit: ArcHabit, from ritual: Ritual) {
|
||||||
guard let arc = ritual.currentArc else { return }
|
guard let arc = ritual.currentArc else { return }
|
||||||
arc.habits.removeAll { $0.id == habit.id }
|
var habits = arc.habits ?? []
|
||||||
|
habits.removeAll { $0.id == habit.id }
|
||||||
|
arc.habits = habits
|
||||||
modelContext.delete(habit)
|
modelContext.delete(habit)
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
@ -649,7 +654,6 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func reloadRituals() {
|
private func reloadRituals() {
|
||||||
PerformanceLogger.measure("RitualStore.reloadRituals") {
|
|
||||||
do {
|
do {
|
||||||
rituals = try modelContext.fetch(FetchDescriptor<Ritual>())
|
rituals = try modelContext.fetch(FetchDescriptor<Ritual>())
|
||||||
updateDerivedData()
|
updateDerivedData()
|
||||||
@ -659,7 +663,6 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
lastErrorMessage = error.localizedDescription
|
lastErrorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func saveContext() {
|
private func saveContext() {
|
||||||
do {
|
do {
|
||||||
@ -692,12 +695,11 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func computeDatesWithActivity() -> Set<Date> {
|
private func computeDatesWithActivity() -> Set<Date> {
|
||||||
PerformanceLogger.measure("RitualStore.computeDatesWithActivity") {
|
|
||||||
var dates: Set<Date> = []
|
var dates: Set<Date> = []
|
||||||
|
|
||||||
for ritual in rituals {
|
for ritual in rituals {
|
||||||
for arc in ritual.arcs {
|
for arc in ritual.arcs ?? [] {
|
||||||
for habit in arc.habits {
|
for habit in arc.habits ?? [] {
|
||||||
for dayID in habit.completedDayIDs {
|
for dayID in habit.completedDayIDs {
|
||||||
if let date = dayFormatter.date(from: dayID) {
|
if let date = dayFormatter.date(from: dayID) {
|
||||||
dates.insert(calendar.startOfDay(for: date))
|
dates.insert(calendar.startOfDay(for: date))
|
||||||
@ -709,7 +711,6 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
return dates
|
return dates
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func computePerfectDays(from activeDates: Set<Date>) -> Set<String> {
|
private func computePerfectDays(from activeDates: Set<Date>) -> Set<String> {
|
||||||
guard !activeDates.isEmpty else { return [] }
|
guard !activeDates.isEmpty else { return [] }
|
||||||
@ -756,7 +757,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
if let ritual = ritual {
|
if let ritual = ritual {
|
||||||
// Get habits from the arc that was active on this date
|
// Get habits from the arc that was active on this date
|
||||||
if let arc = ritual.arc(for: date) {
|
if let arc = ritual.arc(for: date) {
|
||||||
habits = arc.habits
|
habits = arc.habits ?? []
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -792,7 +793,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
if let ritual = ritual {
|
if let ritual = ritual {
|
||||||
// Get habits from the arc that was active on this date
|
// Get habits from the arc that was active on this date
|
||||||
if let arc = ritual.arc(for: date) {
|
if let arc = ritual.arc(for: date) {
|
||||||
for habit in arc.habits {
|
for habit in arc.habits ?? [] {
|
||||||
completions.append(HabitCompletion(
|
completions.append(HabitCompletion(
|
||||||
habit: habit,
|
habit: habit,
|
||||||
ritualTitle: ritual.title,
|
ritualTitle: ritual.title,
|
||||||
@ -804,7 +805,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
// Get all habits from all arcs that were active on this date
|
// Get all habits from all arcs that were active on this date
|
||||||
for r in rituals {
|
for r in rituals {
|
||||||
if let arc = r.arc(for: date) {
|
if let arc = r.arc(for: date) {
|
||||||
for habit in arc.habits {
|
for habit in arc.habits ?? [] {
|
||||||
completions.append(HabitCompletion(
|
completions.append(HabitCompletion(
|
||||||
habit: habit,
|
habit: habit,
|
||||||
ritualTitle: r.title,
|
ritualTitle: r.title,
|
||||||
@ -885,14 +886,16 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
|
|
||||||
/// Returns the current streak for a specific ritual's current arc.
|
/// Returns the current streak for a specific ritual's current arc.
|
||||||
func streakForRitual(_ ritual: Ritual) -> Int {
|
func streakForRitual(_ ritual: Ritual) -> Int {
|
||||||
guard let arc = ritual.currentArc, !arc.habits.isEmpty else { return 0 }
|
guard let arc = ritual.currentArc else { return 0 }
|
||||||
|
let habits = arc.habits ?? []
|
||||||
|
guard !habits.isEmpty else { return 0 }
|
||||||
|
|
||||||
var streak = 0
|
var streak = 0
|
||||||
var checkDate = calendar.startOfDay(for: Date())
|
var checkDate = calendar.startOfDay(for: Date())
|
||||||
|
|
||||||
while arc.contains(date: checkDate) {
|
while arc.contains(date: checkDate) {
|
||||||
let dayID = dayIdentifier(for: checkDate)
|
let dayID = dayIdentifier(for: checkDate)
|
||||||
let allCompleted = arc.habits.allSatisfy { $0.completedDayIDs.contains(dayID) }
|
let allCompleted = habits.allSatisfy { $0.completedDayIDs.contains(dayID) }
|
||||||
|
|
||||||
if allCompleted {
|
if allCompleted {
|
||||||
streak += 1
|
streak += 1
|
||||||
@ -993,7 +996,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
// Update each ritual's arcs to cover a longer period
|
// Update each ritual's arcs to cover a longer period
|
||||||
for ritual in rituals {
|
for ritual in rituals {
|
||||||
// For each arc (active or not), extend it to cover the demo period
|
// For each arc (active or not), extend it to cover the demo period
|
||||||
for arc in ritual.arcs {
|
for arc in ritual.arcs ?? [] {
|
||||||
// Set the arc to start 6 months ago and be active
|
// Set the arc to start 6 months ago and be active
|
||||||
arc.startDate = sixMonthsAgo
|
arc.startDate = sixMonthsAgo
|
||||||
arc.endDate = calendar.date(byAdding: .day, value: 180 + 28 - 1, to: sixMonthsAgo) ?? today
|
arc.endDate = calendar.date(byAdding: .day, value: 180 + 28 - 1, to: sixMonthsAgo) ?? today
|
||||||
@ -1001,7 +1004,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no arcs exist, create one
|
// If no arcs exist, create one
|
||||||
if ritual.arcs.isEmpty {
|
if (ritual.arcs ?? []).isEmpty {
|
||||||
let demoHabits = [
|
let demoHabits = [
|
||||||
ArcHabit(title: "Demo Habit 1", symbolName: "star.fill"),
|
ArcHabit(title: "Demo Habit 1", symbolName: "star.fill"),
|
||||||
ArcHabit(title: "Demo Habit 2", symbolName: "heart.fill")
|
ArcHabit(title: "Demo Habit 2", symbolName: "heart.fill")
|
||||||
@ -1013,7 +1016,9 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
habits: demoHabits
|
habits: demoHabits
|
||||||
)
|
)
|
||||||
ritual.arcs.append(demoArc)
|
var arcs = ritual.arcs ?? []
|
||||||
|
arcs.append(demoArc)
|
||||||
|
ritual.arcs = arcs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1024,11 +1029,11 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
let dayID = dayIdentifier(for: currentDate)
|
let dayID = dayIdentifier(for: currentDate)
|
||||||
|
|
||||||
for ritual in rituals {
|
for ritual in rituals {
|
||||||
for arc in ritual.arcs {
|
for arc in ritual.arcs ?? [] {
|
||||||
// Only generate completions if the arc covers this date
|
// Only generate completions if the arc covers this date
|
||||||
guard arc.contains(date: currentDate) else { continue }
|
guard arc.contains(date: currentDate) else { continue }
|
||||||
|
|
||||||
for habit in arc.habits {
|
for habit in arc.habits ?? [] {
|
||||||
// Random completion with ~70% average success rate
|
// Random completion with ~70% average success rate
|
||||||
let threshold = Double.random(in: 0.5...0.9)
|
let threshold = Double.random(in: 0.5...0.9)
|
||||||
let shouldComplete = Double.random(in: 0...1) < threshold
|
let shouldComplete = Double.random(in: 0...1) < threshold
|
||||||
@ -1049,8 +1054,8 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
/// Clears all completion data (for testing).
|
/// Clears all completion data (for testing).
|
||||||
func clearAllCompletions() {
|
func clearAllCompletions() {
|
||||||
for ritual in rituals {
|
for ritual in rituals {
|
||||||
for arc in ritual.arcs {
|
for arc in ritual.arcs ?? [] {
|
||||||
for habit in arc.habits {
|
for habit in arc.habits ?? [] {
|
||||||
habit.completedDayIDs.removeAll()
|
habit.completedDayIDs.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,12 +88,10 @@ struct HistoryMonthView: View {
|
|||||||
private var dayGrid: some View {
|
private var dayGrid: some View {
|
||||||
let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7)
|
let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7)
|
||||||
let dates = daysInMonth
|
let dates = daysInMonth
|
||||||
let progressValues = PerformanceLogger.measure("HistoryMonthView.progressValues.\(monthTitle)") {
|
let progressValues = dates.map { date -> Double? in
|
||||||
dates.map { date -> Double? in
|
|
||||||
guard let date else { return nil }
|
guard let date else { return nil }
|
||||||
return date > today ? 0 : completionRate(date, selectedRitual)
|
return date > today ? 0 : completionRate(date, selectedRitual)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return LazyVGrid(columns: columns, spacing: Design.Spacing.xSmall) {
|
return LazyVGrid(columns: columns, spacing: Design.Spacing.xSmall) {
|
||||||
ForEach(Array(dates.enumerated()), id: \.offset) { index, date in
|
ForEach(Array(dates.enumerated()), id: \.offset) { index, date in
|
||||||
|
|||||||
@ -32,7 +32,6 @@ struct HistoryView: View {
|
|||||||
/// - Expanded: Up to 12 months of history
|
/// - Expanded: Up to 12 months of history
|
||||||
/// Months are ordered oldest first, newest last (chronological order)
|
/// Months are ordered oldest first, newest last (chronological order)
|
||||||
private var months: [Date] {
|
private var months: [Date] {
|
||||||
PerformanceLogger.measure("HistoryView.months") {
|
|
||||||
let today = Date()
|
let today = Date()
|
||||||
let currentMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: today)) ?? today
|
let currentMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: today)) ?? today
|
||||||
|
|
||||||
@ -55,7 +54,6 @@ struct HistoryView: View {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if there's more history available beyond what's shown
|
/// Check if there's more history available beyond what's shown
|
||||||
private var hasMoreHistory: Bool {
|
private var hasMoreHistory: Bool {
|
||||||
@ -200,7 +198,6 @@ struct HistoryView: View {
|
|||||||
await Task.yield()
|
await Task.yield()
|
||||||
let snapshotMonths = months
|
let snapshotMonths = months
|
||||||
let selected = selectedRitual
|
let selected = selectedRitual
|
||||||
let cache = PerformanceLogger.measure("HistoryView.progressCache") {
|
|
||||||
var result: [Date: Double] = [:]
|
var result: [Date: Double] = [:]
|
||||||
let today = calendar.startOfDay(for: Date())
|
let today = calendar.startOfDay(for: Date())
|
||||||
|
|
||||||
@ -217,9 +214,7 @@ struct HistoryView: View {
|
|||||||
result[normalizedDate] = store.completionRate(for: normalizedDate, ritual: selected)
|
result[normalizedDate] = store.completionRate(for: normalizedDate, ritual: selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let cache = result
|
||||||
return result
|
|
||||||
}
|
|
||||||
cachedProgressByDate = cache
|
cachedProgressByDate = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,11 +36,11 @@ struct RitualDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var hasMultipleArcs: Bool {
|
private var hasMultipleArcs: Bool {
|
||||||
ritual.arcs.count > 1
|
(ritual.arcs ?? []).count > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private var completedArcs: [RitualArc] {
|
private var completedArcs: [RitualArc] {
|
||||||
ritual.arcs.filter { !$0.isActive }.sorted { $0.startDate > $1.startDate }
|
(ritual.arcs ?? []).filter { !$0.isActive }.sorted { $0.startDate > $1.startDate }
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -353,7 +353,7 @@ struct RitualDetailView: View {
|
|||||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||||
SectionHeaderView(
|
SectionHeaderView(
|
||||||
title: String(localized: "Arc History"),
|
title: String(localized: "Arc History"),
|
||||||
subtitle: ritual.arcs.isEmpty ? nil : String(localized: "\(ritual.arcs.count) total")
|
subtitle: (ritual.arcs ?? []).isEmpty ? nil : String(localized: "\((ritual.arcs ?? []).count) total")
|
||||||
)
|
)
|
||||||
|
|
||||||
if completedArcs.isEmpty {
|
if completedArcs.isEmpty {
|
||||||
@ -374,8 +374,9 @@ struct RitualDetailView: View {
|
|||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateStyle = .medium
|
dateFormatter.dateStyle = .medium
|
||||||
|
|
||||||
let totalCheckIns = arc.habits.reduce(0) { $0 + $1.completedDayIDs.count }
|
let habits = arc.habits ?? []
|
||||||
let possibleCheckIns = arc.habits.count * arc.durationDays
|
let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count }
|
||||||
|
let possibleCheckIns = habits.count * arc.durationDays
|
||||||
let completionRate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
|
let completionRate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
|
||||||
|
|
||||||
return HStack {
|
return HStack {
|
||||||
|
|||||||
@ -24,8 +24,9 @@ struct ArcRenewalSheet: View {
|
|||||||
|
|
||||||
private var arcSummary: String {
|
private var arcSummary: String {
|
||||||
guard let arc = completedArc else { return "" }
|
guard let arc = completedArc else { return "" }
|
||||||
let totalHabits = arc.habits.count
|
let habits = arc.habits ?? []
|
||||||
let totalCheckIns = arc.habits.reduce(0) { $0 + $1.completedDayIDs.count }
|
let totalHabits = habits.count
|
||||||
|
let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count }
|
||||||
let possibleCheckIns = totalHabits * arc.durationDays
|
let possibleCheckIns = totalHabits * arc.durationDays
|
||||||
let rate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
|
let rate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
|
||||||
return String(localized: "\(rate)% completion over \(arc.durationDays) days")
|
return String(localized: "\(rate)% completion over \(arc.durationDays) days")
|
||||||
@ -94,7 +95,7 @@ struct ArcRenewalSheet: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
||||||
if let arc = completedArc {
|
if let arc = completedArc {
|
||||||
let habitCount = arc.habits.count
|
let habitCount = (arc.habits ?? []).count
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bedrock
|
import Bedrock
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
@ -85,9 +84,7 @@ struct RootView: View {
|
|||||||
Task {
|
Task {
|
||||||
// Let tab selection UI update before refreshing data.
|
// Let tab selection UI update before refreshing data.
|
||||||
await Task.yield()
|
await Task.yield()
|
||||||
let refreshStart = CFAbsoluteTimeGetCurrent()
|
|
||||||
store.refresh()
|
store.refresh()
|
||||||
PerformanceLogger.logDuration("RootView.refreshCurrentTab.store.refresh", from: refreshStart)
|
|
||||||
analyticsPrewarmTask?.cancel()
|
analyticsPrewarmTask?.cancel()
|
||||||
if selectedTab != .insights {
|
if selectedTab != .insights {
|
||||||
analyticsPrewarmTask = Task { @MainActor in
|
analyticsPrewarmTask = Task { @MainActor in
|
||||||
@ -98,12 +95,8 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selectedTab == .settings {
|
if selectedTab == .settings {
|
||||||
let settingsStart = CFAbsoluteTimeGetCurrent()
|
|
||||||
settingsStore.refresh()
|
settingsStore.refresh()
|
||||||
PerformanceLogger.logDuration("RootView.refreshCurrentTab.settings.refresh", from: settingsStart)
|
|
||||||
let reminderStart = CFAbsoluteTimeGetCurrent()
|
|
||||||
await store.reminderScheduler.refreshStatus()
|
await store.reminderScheduler.refreshStatus()
|
||||||
PerformanceLogger.logDuration("RootView.refreshCurrentTab.reminderStatus", from: reminderStart)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user