Skip to content

Commit bc51b44

Browse files
committed
Seed test data; refine calendar & log UI
Add a programmatic TestDataSeeder and invoke it on app launch to populate sample cycles/logs for development (toggleable via isEnabled, avoids double-seeding and logs success/failure). Adjust NativeCalendarView decorations to deduplicate decorated dates, use a custom view for period decoration, and tweak colors/alpha for predicted/fertile states. Simplify and reformat LogView form layout (remove inner NavigationStack, tidy HStacks/padding), preserve notes character limit and save-on-disappear behavior, and keep the dismiss toolbar. Update project checklist to reflect the native calendar, TestDataSeeder, and stability fixes.
1 parent e6d0a92 commit bc51b44

5 files changed

Lines changed: 173 additions & 80 deletions

File tree

CycleOne/App/CycleOneApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ struct CycleOneApp: App {
2020
.environment(\.managedObjectContext, persistenceController.container.viewContext)
2121
.environmentObject(themeManager)
2222
.preferredColorScheme(themeManager.selectedTheme.colorScheme)
23+
.onAppear {
24+
TestDataSeeder.seed(context: persistenceController.container.viewContext)
25+
}
2326
}
2427
}
2528
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// TestDataSeeder.swift
3+
// CycleOne
4+
//
5+
6+
import CoreData
7+
import Foundation
8+
import OSLog
9+
10+
enum TestDataSeeder {
11+
/// Toggle this to enable/disable test data seeding programmatically.
12+
static let isEnabled = true
13+
14+
static func seed(context: NSManagedObjectContext) {
15+
guard isEnabled else { return }
16+
17+
// Check if data already exists to avoid double seeding
18+
let request: NSFetchRequest<Cycle> = Cycle.fetchRequest()
19+
do {
20+
let count = try context.count(for: request)
21+
if count > 0 { return }
22+
23+
Logger.storage.info("🌱 Seeding test data...")
24+
25+
let calendar = Calendar.current
26+
let today = Date().startOfDay
27+
28+
// 1. Past Cycle (28 days ago)
29+
guard let pastStartDate = calendar.date(byAdding: .day, value: -28, to: today) else { return }
30+
let cycle1 = Cycle(context: context)
31+
cycle1.id = UUID()
32+
cycle1.startDate = pastStartDate
33+
cycle1.periodLength = 5
34+
cycle1.cycleLength = 28
35+
36+
// Create logs for past period
37+
for index in 0 ..< 5 {
38+
guard let logDate = calendar.date(byAdding: .day, value: index, to: pastStartDate) else { continue }
39+
let log = DayLog(context: context)
40+
log.id = UUID()
41+
log.date = logDate
42+
log.flowLevel = index == 2 ? 3 : 1 // Heavy on day 3
43+
log.mood = Int16(index % 5)
44+
log.energyLevel = 1
45+
46+
// Add a symptom to some days
47+
if index % 2 == 0 {
48+
let symptom = Symptom(context: context)
49+
symptom.id = "cramps"
50+
symptom.name = "Cramps"
51+
symptom.category = SymptomCategory.physical.rawValue
52+
symptom.dayLog = log
53+
}
54+
}
55+
56+
// 2. Current Cycle (Started 2 days ago)
57+
let currentStartDate = today.addingTimeInterval(-86400 * 2)
58+
let cycle2 = Cycle(context: context)
59+
cycle2.id = UUID()
60+
cycle2.startDate = currentStartDate
61+
cycle2.periodLength = 5
62+
63+
for index in 0 ..< 3 {
64+
guard let logDate = calendar.date(byAdding: .day, value: index, to: currentStartDate) else { continue }
65+
let log = DayLog(context: context)
66+
log.id = UUID()
67+
log.date = logDate
68+
log.flowLevel = 2 // Medium
69+
log.mood = 1 // Neutral
70+
log.energyLevel = 1
71+
log.notes = "Seeded test entry \(index + 1)"
72+
}
73+
74+
try context.save()
75+
Logger.storage.info("✅ Test data seeded successfully.")
76+
} catch {
77+
Logger.storage.error("❌ Failed to seed test data: \(error.localizedDescription)")
78+
}
79+
}
80+
}

CycleOne/Views/Calendar/NativeCalendarView.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,29 @@ struct NativeCalendarView: UIViewRepresentable {
6464
decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration?
6565
{
6666
guard let date = Calendar.current.date(from: dateComponents) else { return nil }
67-
let status = parent.viewModel.dayStatuses[date.startOfDay] ?? DayStatus()
6867

69-
lastDecoratedDates.append(dateComponents)
68+
if !lastDecoratedDates.contains(dateComponents) {
69+
lastDecoratedDates.append(dateComponents)
70+
}
71+
72+
let status = parent.viewModel.dayStatuses[date.startOfDay] ?? DayStatus()
7073

74+
// Multiple decorations if needed
7175
if status.flow != .none {
72-
return .default(color: .systemPink, size: .medium)
76+
return .customView {
77+
let view = UIView()
78+
view.backgroundColor = UIColor.systemPink // Standard period color
79+
view.layer.cornerRadius = 6 // Smaller circle for decoration area
80+
view.alpha = 0.6
81+
view.frame = CGRect(x: 0, y: 0, width: 12, height: 12)
82+
return view
83+
}
7384
} else if status.isPredicted {
74-
return .default(color: .systemPink.withAlphaComponent(0.4), size: .small)
85+
return .default(color: .systemGray, size: .small)
7586
} else if status.isOvulation {
7687
return .default(color: .systemTeal, size: .medium)
7788
} else if status.isFertile {
78-
return .default(color: .systemTeal.withAlphaComponent(0.4), size: .small)
89+
return .default(color: UIColor.systemTeal.withAlphaComponent(0.3), size: .small)
7990
} else if status.hasLogs {
8091
return .default(color: .secondaryLabel, size: .small)
8192
}

CycleOne/Views/Log/LogView.swift

Lines changed: 70 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,94 +15,92 @@ struct LogView: View {
1515
}
1616

1717
var body: some View {
18-
NavigationStack {
19-
Form {
20-
Section("Flow") {
21-
FlowPickerView(selection: $viewModel.flow)
22-
}
18+
Form {
19+
Section("Flow") {
20+
FlowPickerView(selection: $viewModel.flow)
21+
}
2322

24-
Section("Mood & Energy") {
25-
HStack {
26-
ForEach(Mood.allCases, id: \.self) { mood in
27-
VStack {
28-
Image(systemName: mood.icon)
29-
.font(.title2)
30-
.padding(8)
31-
.background(viewModel.mood == mood ? Color.themeAccent.opacity(0.2) : Color.clear)
32-
.clipShape(Circle())
33-
Text(mood.description)
34-
.font(.caption2)
35-
}
36-
.frame(maxWidth: .infinity)
37-
.onTapGesture { viewModel.mood = mood }
23+
Section("Mood & Energy") {
24+
HStack {
25+
ForEach(Mood.allCases, id: \.self) { mood in
26+
VStack {
27+
Image(systemName: mood.icon)
28+
.font(.title2)
29+
.padding(8)
30+
.background(viewModel.mood == mood ? Color.themeAccent.opacity(0.2) : Color.clear)
31+
.clipShape(Circle())
32+
Text(mood.description)
33+
.font(.caption2)
3834
}
35+
.frame(maxWidth: .infinity)
36+
.onTapGesture { viewModel.mood = mood }
3937
}
40-
.padding(.vertical, 8)
38+
}
39+
.padding(.vertical, 8)
4140

42-
HStack {
43-
ForEach(EnergyLevel.allCases, id: \.self) { energy in
44-
VStack {
45-
Image(systemName: energy.icon)
46-
.font(.title2)
47-
.padding(8)
48-
.background(viewModel.energy == energy ? Color.themeAccent.opacity(0.2) : Color
49-
.clear)
50-
.clipShape(Circle())
51-
Text(energy.description)
52-
.font(.caption2)
53-
}
54-
.frame(maxWidth: .infinity)
55-
.onTapGesture { viewModel.energy = energy }
41+
HStack {
42+
ForEach(EnergyLevel.allCases, id: \.self) { energy in
43+
VStack {
44+
Image(systemName: energy.icon)
45+
.font(.title2)
46+
.padding(8)
47+
.background(viewModel.energy == energy ? Color.themeAccent.opacity(0.2) : Color
48+
.clear)
49+
.clipShape(Circle())
50+
Text(energy.description)
51+
.font(.caption2)
5652
}
53+
.frame(maxWidth: .infinity)
54+
.onTapGesture { viewModel.energy = energy }
5755
}
58-
.padding(.vertical, 8)
5956
}
57+
.padding(.vertical, 8)
58+
}
6059

61-
Section("Pain Level: \(Int(viewModel.painLevel))") {
62-
Slider(value: $viewModel.painLevel, in: 0 ... 10, step: 1)
63-
.accentColor(.themePeriod)
64-
}
60+
Section("Pain Level: \(Int(viewModel.painLevel))") {
61+
Slider(value: $viewModel.painLevel, in: 0 ... 10, step: 1)
62+
.accentColor(.themePeriod)
63+
}
6564

66-
Section("Symptoms") {
67-
SymptomGridView(selectedSymptoms: $viewModel.selectedSymptoms, symptoms: SymptomType.defaults)
68-
}
65+
Section("Symptoms") {
66+
SymptomGridView(selectedSymptoms: $viewModel.selectedSymptoms, symptoms: SymptomType.defaults)
67+
}
6968

70-
Section {
71-
ZStack(alignment: .topLeading) {
72-
if viewModel.notes.isEmpty {
73-
Text("Empty")
74-
.foregroundColor(.secondary.opacity(0.5))
75-
.padding(.horizontal, 4)
76-
.padding(.vertical, 8)
77-
}
69+
Section {
70+
ZStack(alignment: .topLeading) {
71+
if viewModel.notes.isEmpty {
72+
Text("Empty")
73+
.foregroundColor(.secondary.opacity(0.5))
74+
.padding(.horizontal, 4)
75+
.padding(.vertical, 8)
76+
}
7877

79-
TextEditor(text: $viewModel.notes)
80-
.frame(minHeight: 100)
81-
.onChange(of: viewModel.notes) { _, newValue in
82-
if newValue.count > 500 {
83-
viewModel.notes = String(newValue.prefix(500))
84-
}
78+
TextEditor(text: $viewModel.notes)
79+
.frame(minHeight: 100)
80+
.onChange(of: viewModel.notes) { _, newValue in
81+
if newValue.count > 500 {
82+
viewModel.notes = String(newValue.prefix(500))
8583
}
86-
}
87-
} header: {
88-
HStack {
89-
Text("Notes")
90-
Image(systemName: "pencil")
91-
.font(.caption)
92-
}
84+
}
9385
}
94-
}
95-
.navigationTitle("Log Day")
96-
.navigationBarTitleDisplayMode(.inline)
97-
.toolbar {
98-
ToolbarItem(placement: .navigationBarTrailing) {
99-
Button("Dismiss") { dismiss() }
100-
.fontWeight(.medium)
86+
} header: {
87+
HStack {
88+
Text("Notes")
89+
Image(systemName: "pencil")
90+
.font(.caption)
10191
}
10292
}
103-
.onDisappear {
104-
viewModel.save()
93+
}
94+
.navigationTitle("Log Day")
95+
.navigationBarTitleDisplayMode(.inline)
96+
.toolbar {
97+
ToolbarItem(placement: .navigationBarTrailing) {
98+
Button("Dismiss") { dismiss() }
99+
.fontWeight(.medium)
105100
}
106101
}
102+
.onDisappear {
103+
viewModel.save()
104+
}
107105
}
108106
}

CycleOneDocs/cycleone_checklist.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@
3939
- [x] **Theming**: System/Light/Dark mode switcher
4040
- [x] **Stability**: Fixed layout frames for cards
4141

42-
## Robust Overhaul (Phase 8 & 9)
43-
- [x] **Native Calendar**: `UICalendarView` with decorations
42+
## Robust Overhaul (Phase 8, 9 & 10)
43+
- [x] **Native Calendar**: `UICalendarView` with red circle decorations
4444
- [x] **Logging Flow**: Navigation-based logging (No modals)
4545
- [x] **Auto-Save**: Automatic persistence on dismiss
4646
- [x] **UX Polish**: Scrollable main screen, energy highlights
47-
- [x] **Stability**: Fixed symptom selection & test suite refinement
47+
- [x] **TestDataSeeder**: Programmatic test data seeding
48+
- [x] **Stability**: Fixed symptom selection & navigation bugs

0 commit comments

Comments
 (0)