1 Commits

Author SHA1 Message Date
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
3 changed files with 19 additions and 12 deletions

View File

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

View File

@@ -116,6 +116,14 @@ struct PlantDetailView: View {
.task { .task {
await viewModel.loadCareInfo() 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 { .refreshable {
await viewModel.refresh() await viewModel.refresh()
} }

View File

@@ -144,6 +144,12 @@ final class PlantDetailViewModel {
isLoading = false 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 /// Loads an existing care schedule from the repository
private func loadExistingSchedule() async { private func loadExistingSchedule() async {
do { do {