Merge pull request #18 from akatreyt/fix/issue-17

fix: issue #17 - upcoming tasks
This commit is contained in:
akatreyt
2026-03-10 09:41:31 -05:00
committed by GitHub
8 changed files with 54 additions and 36 deletions

View File

@@ -469,13 +469,15 @@ extension CoreDataStack {
func resetForTesting() throws {
let context = viewContext()
// Delete all entities
// Delete all entities individually (NSBatchDeleteRequest is unsupported on in-memory stores)
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
for entityName in entityNames {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(deleteRequest)
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
let objects = try context.fetch(fetchRequest)
for object in objects {
context.delete(object)
}
}
try save(context: context)

View File

@@ -84,15 +84,15 @@ final class FilterPreferencesStorage: FilterPreferencesStorageProtocol, @uncheck
// Save sortAscending
userDefaults.set(filter.sortAscending, forKey: Keys.filterSortAscending)
// Save families (as array of strings)
if let families = filter.families {
// Save families (as array of strings) empty set treated as nil (no filter)
if let families = filter.families, !families.isEmpty {
userDefaults.set(Array(families), forKey: Keys.filterFamilies)
} else {
userDefaults.removeObject(forKey: Keys.filterFamilies)
}
// Save light requirements (as array of raw values)
if let lightRequirements = filter.lightRequirements {
// Save light requirements (as array of raw values) empty set treated as nil
if let lightRequirements = filter.lightRequirements, !lightRequirements.isEmpty {
let rawValues = lightRequirements.map { $0.rawValue }
userDefaults.set(rawValues, forKey: Keys.filterLightRequirements)
} else {

View File

@@ -106,6 +106,9 @@ final class CollectionViewModel {
/// Task for debounced search
private var searchTask: Task<Void, Never>?
/// Active load task to prevent duplicate concurrent loads
private var currentLoadTask: Task<Void, Never>?
/// Search debounce interval in nanoseconds (300ms)
private let searchDebounceNanoseconds: UInt64 = 300_000_000
@@ -156,21 +159,28 @@ final class CollectionViewModel {
/// This method fetches all plants based on the current filter and updates
/// the view model's state accordingly. Shows loading state during fetch.
func loadPlants() async {
guard !isLoading else { return }
if let currentLoadTask {
await currentLoadTask.value
return
}
isLoading = true
error = nil
do {
let fetchedPlants = try await fetchCollectionUseCase.execute(filter: currentFilter)
allPlants = fetchedPlants
applySearchFilter()
} catch {
self.error = error.localizedDescription
plants = []
allPlants = []
let task = Task { @MainActor in
do {
let fetchedPlants = try await self.fetchCollectionUseCase.execute(filter: self.currentFilter)
self.allPlants = fetchedPlants
self.applySearchFilter()
} catch {
self.error = error.localizedDescription
self.plants = []
self.allPlants = []
}
}
currentLoadTask = task
await task.value
currentLoadTask = nil
isLoading = false
}

View File

@@ -148,8 +148,8 @@ final class CareScheduleViewModel {
do {
try await careScheduleRepository.updateTask(completedTask)
} catch {
// Log error - in a production app, you might want to show an alert
print("Failed to persist task completion: \(error)")
// Revert in-memory state so UI stays consistent with Core Data
allTasks[index] = task
}
}

View File

@@ -133,13 +133,14 @@ final class PlantDetailViewModel {
forceRefresh: forceRefresh
)
careInfo = info
// Check for existing schedule
await loadExistingSchedule()
} catch {
self.error = error
}
// Always load the existing schedule from Core Data, even if care info
// fetch failed. This ensures task completion states are always current.
await loadExistingSchedule()
isLoading = false
}
@@ -298,7 +299,10 @@ final class PlantDetailViewModel {
do {
try await careScheduleRepository.updateTask(completedTask)
} catch {
print("Failed to persist task completion: \(error)")
// Revert in-memory state so UI stays consistent with Core Data
schedule.tasks[index] = task
careSchedule = schedule
self.error = error
}
}
}

View File

@@ -216,8 +216,8 @@ final class TodayViewModel {
do {
try await careScheduleRepository.updateTask(completedTask)
} catch {
// Log error - in a production app, you might want to show an alert
print("Failed to persist task completion: \(error)")
// Revert in-memory state so UI stays consistent with Core Data
allTasks[index] = task
}
}

View File

@@ -86,9 +86,11 @@ final class MockCoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
for entityName in entityNames {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(deleteRequest)
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
let objects = try context.fetch(fetchRequest)
for object in objects {
context.delete(object)
}
}
try save(context: context)

View File

@@ -143,7 +143,7 @@ struct MockImagePreprocessor: ImagePreprocessorProtocol, Sendable {
// MARK: - MockIdentifyPlantUseCase
/// Mock implementation of IdentifyPlantUseCaseProtocol for testing
struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
final class MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, @unchecked Sendable {
// MARK: - Configuration
@@ -167,7 +167,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
/// Creates a mock that returns high-confidence predictions
static func withHighConfidencePredictions() -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
let mock = MockIdentifyPlantUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
@@ -181,7 +181,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
/// Creates a mock that returns low-confidence predictions
static func withLowConfidencePredictions() -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
let mock = MockIdentifyPlantUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
@@ -195,7 +195,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
/// Creates a mock that throws an error
static func withError(_ error: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound) -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
let mock = MockIdentifyPlantUseCase()
mock.shouldThrow = true
mock.errorToThrow = error
return mock
@@ -205,7 +205,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
// MARK: - MockIdentifyPlantOnlineUseCase
/// Mock implementation of IdentifyPlantOnlineUseCaseProtocol for testing
struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Sendable {
final class MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, @unchecked Sendable {
// MARK: - Configuration
@@ -229,7 +229,7 @@ struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Senda
/// Creates a mock that returns API predictions
static func withPredictions() -> MockIdentifyPlantOnlineUseCase {
var mock = MockIdentifyPlantOnlineUseCase()
let mock = MockIdentifyPlantOnlineUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
@@ -243,7 +243,7 @@ struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Senda
/// Creates a mock that throws an error
static func withError(_ error: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound) -> MockIdentifyPlantOnlineUseCase {
var mock = MockIdentifyPlantOnlineUseCase()
let mock = MockIdentifyPlantOnlineUseCase()
mock.shouldThrow = true
mock.errorToThrow = error
return mock