- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.2 KiB
Plan: Persist PlantCareInfo in Core Data
Overview
Cache Trefle API care info locally so API is only called once per plant. Preserves all timing info (watering frequency, fertilizer schedule) for proper notification scheduling.
Current Problem
PlantCareInfois fetched from Trefle API every timePlantDetailViewappears- No local caching - unnecessary API calls and poor offline experience
Solution
Add PlantCareInfoMO Core Data entity with cache-first logic in FetchPlantCareUseCase.
Implementation Steps
Step 1: Add Value Transformers for Complex Types
File: PlantGuide/Core/Utilities/ValueTransformers.swift
Add JSON-based transformers (following existing IdentificationResultArrayTransformer pattern):
WateringScheduleTransformer- encodesWateringSchedulestructTemperatureRangeTransformer- encodesTemperatureRangestructFertilizerScheduleTransformer- encodesFertilizerSchedulestructSeasonArrayTransformer- encodes[Season]array
Register all transformers in PlantGuideApp.swift init.
Step 2: Update Core Data Model
File: PlantGuide/Data/DataSources/Local/CoreData/PlantGuideModel.xcdatamodeld
Add new entity PlantCareInfoMO:
| Attribute | Type | Notes |
|---|---|---|
id |
UUID | Required, unique |
scientificName |
String | Required |
commonName |
String | Optional |
lightRequirement |
String | Enum rawValue |
wateringScheduleData |
Binary | JSON-encoded WateringSchedule |
temperatureRangeData |
Binary | JSON-encoded TemperatureRange |
fertilizerScheduleData |
Binary | Optional, JSON-encoded |
humidity |
String | Optional, enum rawValue |
growthRate |
String | Optional, enum rawValue |
bloomingSeasonData |
Binary | Optional, JSON-encoded [Season] |
additionalNotes |
String | Optional |
sourceURL |
URI | Optional |
trefleID |
Integer 32 | Optional |
fetchedAt |
Date | Required, for cache expiration |
Relationships:
plant→PlantMO(optional, one-to-one, inverse:plantCareInfo)
Update PlantMO:
- Add relationship
plantCareInfo→PlantCareInfoMO(optional, cascade delete)
Step 3: Create PlantCareInfoMO Managed Object
File: PlantGuide/Data/DataSources/Local/CoreData/ManagedObjects/PlantCareInfoMO.swift (NEW)
- Define
@NSManagedproperties - Add
toDomainModel() -> PlantCareInfo?- decodes JSON data to domain structs - Add
static func fromDomainModel(_:context:) -> PlantCareInfoMO?- encodes domain to MO - Add
func update(from:)- updates existing MO
Step 4: Create Repository Protocol and Implementation
File: PlantGuide/Domain/RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift (NEW)
protocol PlantCareInfoRepositoryProtocol: Sendable {
func fetch(scientificName: String) async throws -> PlantCareInfo?
func fetch(trefleID: Int) async throws -> PlantCareInfo?
func fetch(for plantID: UUID) async throws -> PlantCareInfo?
func save(_ careInfo: PlantCareInfo, for plantID: UUID?) async throws
func isCacheStale(scientificName: String, cacheExpiration: TimeInterval) async throws -> Bool
func delete(for plantID: UUID) async throws
}
File: PlantGuide/Data/DataSources/Local/CoreData/CoreDataPlantCareInfoStorage.swift (NEW)
Implement repository with Core Data queries.
Step 5: Update FetchPlantCareUseCase with Cache-First Logic
File: PlantGuide/Domain/UseCases/PlantCare/FetchPlantCareUseCase.swift
Modify to:
- Inject
PlantCareInfoRepositoryProtocol - Check cache first before API call
- Validate cache freshness (7-day expiration)
- Save API response to cache after fetch
func execute(scientificName: String) async throws -> PlantCareInfo {
// 1. Check cache
if let cached = try await repository.fetch(scientificName: scientificName),
!(try await repository.isCacheStale(scientificName: scientificName, cacheExpiration: 7 * 24 * 60 * 60)) {
return cached
}
// 2. Fetch from API
let careInfo = try await fetchFromAPI(scientificName: scientificName)
// 3. Cache result
try await repository.save(careInfo, for: nil)
return careInfo
}
Step 6: Update DIContainer
File: PlantGuide/Core/DI/DIContainer.swift
- Add
_plantCareInfoStoragelazy service - Add
plantCareInfoRepositoryaccessor - Update
_fetchPlantCareUseCaseto inject repository - Add to
resetAll()method
Step 7: Update PlantMO
File: PlantGuide/Data/DataSources/Local/CoreData/ManagedObjects/PlantMO.swift
Add relationship property:
@NSManaged public var plantCareInfo: PlantCareInfoMO?
Migration Strategy
Lightweight migration - no custom mapping model needed:
- New entity with no existing data
- New relationship is optional (nil default)
shouldMigrateStoreAutomaticallyandshouldInferMappingModelAutomaticallyalready enabled
Files to Create/Modify
| File | Action |
|---|---|
Core/Utilities/ValueTransformers.swift |
Add 4 transformers |
PlantGuideModel.xcdatamodel/contents |
Add PlantCareInfoMO entity |
ManagedObjects/PlantCareInfoMO.swift |
NEW - managed object + mappers |
RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift |
NEW - protocol |
CoreData/CoreDataPlantCareInfoStorage.swift |
NEW - implementation |
UseCases/PlantCare/FetchPlantCareUseCase.swift |
Add cache-first logic |
DI/DIContainer.swift |
Register new dependencies |
ManagedObjects/PlantMO.swift |
Add relationship |
App/PlantGuideApp.swift |
Register new transformers |
Verification
-
Build verification:
xcodebuild -scheme PlantGuide build -
Test cache behavior:
- Add a new plant → view details (should call Trefle API)
- Navigate away and back to details (should NOT call API - use cache)
- Check console logs for API calls
-
Test timing preservation:
- Verify watering frequency
intervalDaysproperty works after cache retrieval - Create care schedule from cached info → verify notifications scheduled correctly
- Verify watering frequency
-
Test cache expiration:
- Manually set
fetchedAtto 8 days ago - View plant details → should re-fetch from API
- Manually set
-
Run existing tests:
xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 17'