4 Commits

Author SHA1 Message Date
98244b3d65 Merge pull request 'fix: completed tasks reappearing as overdue (#2)' (#4) from fix/issue-2-task-completion-persistence into master
Reviewed-on: #4
2026-04-12 10:14:26 -05:00
42084b6601 Merge pull request 'fix: Save to Collection button not responding (#1)' (#3) from fix/issue-1-save-to-collection into master
Reviewed-on: #3
2026-04-12 10:14:14 -05:00
Trey t
c1c824f288 fix: completed care tasks reappearing as overdue after reopening
When a user marked a care task as complete, the task would disappear
from the upcoming tasks section. However, upon navigating away and
returning to the plant detail, the task would reappear as incomplete
and overdue.

The root cause was that PlantDetailView only used .task to load
schedule data, which runs once on first appearance. When the view was
recreated (e.g., after navigating back from the collection list), the
Core Data fetch could return stale data due to context isolation in
NSPersistentCloudKitContainer.

Added .onAppear to reload the care schedule from Core Data every time
the view appears, matching the pattern already used in TodayView.
Also exposed a refreshSchedule() method on the ViewModel for this
purpose.

Fixes #2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:13:08 -05:00
Trey t
987ebf9690 fix: Save to Collection button not responding without user interaction
The Save to Collection button on the identification screen would remain
disabled after plant identification completed, requiring the user to tap
a prediction row before it would enable. This was caused by an @Observable
+ @State tracking issue where computed properties in SwiftUI view modifiers
don't always trigger re-renders when the underlying observable changes.

Replaced the empty .onChange workaround with a local @State property
(saveEnabled) that is explicitly updated when selectedPrediction or
saveState changes, ensuring the button state always reflects the current
ViewModel state.

Fixes #1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:08:30 -05:00
3 changed files with 26 additions and 5 deletions

View File

@@ -19,6 +19,11 @@ struct IdentificationView: View {
/// Tracks whether we've announced results to avoid duplicate announcements
@State private var hasAnnouncedResults = false
/// Local state to reliably drive the save button's enabled/disabled state.
/// Works around an @Observable + @State tracking issue where computed
/// properties in view modifiers don't always trigger re-renders.
@State private var saveEnabled = false
// MARK: - Scaled Metrics for Dynamic Type
@ScaledMetric(relativeTo: .body) private var closeIconSize: CGFloat = 16
@@ -76,8 +81,10 @@ struct IdentificationView: View {
announceStateChange(from: oldValue, to: newValue)
}
.onChange(of: viewModel.selectedPrediction?.id) { _, _ in
// Force view update when selection changes
// This ensures SwiftUI tracks @Observable property changes correctly
saveEnabled = viewModel.canSaveToCollection
}
.onChange(of: viewModel.saveState) { _, _ in
saveEnabled = viewModel.canSaveToCollection
}
.accessibilityIdentifier(AccessibilityIdentifiers.Identification.identificationView)
.alert("Plant Saved!", isPresented: .init(
@@ -406,12 +413,12 @@ struct IdentificationView: View {
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(viewModel.canSaveToCollection ? Color.accentColor : Color.gray)
.fill(saveEnabled ? Color.accentColor : Color.gray)
)
}
.disabled(!viewModel.canSaveToCollection)
.disabled(!saveEnabled)
.accessibilityLabel(viewModel.saveState == .saving ? "Saving plant" : "Save to Collection")
.accessibilityHint(viewModel.canSaveToCollection ? "Saves the selected plant to your collection" : "Select a plant first")
.accessibilityHint(saveEnabled ? "Saves the selected plant to your collection" : "Select a plant first")
.accessibilityIdentifier(AccessibilityIdentifiers.Identification.saveToCollectionButton)
}
.padding(.horizontal, 20)

View File

@@ -116,6 +116,14 @@ struct PlantDetailView: View {
.task {
await viewModel.loadCareInfo()
}
.onAppear {
// Reload schedule from Core Data every time the view appears.
// .task only runs on first appearance; this ensures task completion
// states are always current when navigating back to this view.
Task {
await viewModel.refreshSchedule()
}
}
.refreshable {
await viewModel.refresh()
}

View File

@@ -144,6 +144,12 @@ final class PlantDetailViewModel {
isLoading = false
}
/// Reloads the care schedule from the repository.
/// Call on view reappearance to pick up persisted task completions.
func refreshSchedule() async {
await loadExistingSchedule()
}
/// Loads an existing care schedule from the repository
private func loadExistingSchedule() async {
do {