- 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>
168 lines
6.2 KiB
Markdown
168 lines
6.2 KiB
Markdown
# 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
|
|
- `PlantCareInfo` is fetched from Trefle API every time `PlantDetailView` appears
|
|
- 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` - encodes `WateringSchedule` struct
|
|
- `TemperatureRangeTransformer` - encodes `TemperatureRange` struct
|
|
- `FertilizerScheduleTransformer` - encodes `FertilizerSchedule` struct
|
|
- `SeasonArrayTransformer` - 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 `@NSManaged` properties
|
|
- 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)
|
|
|
|
```swift
|
|
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:
|
|
1. Inject `PlantCareInfoRepositoryProtocol`
|
|
2. Check cache first before API call
|
|
3. Validate cache freshness (7-day expiration)
|
|
4. Save API response to cache after fetch
|
|
|
|
```swift
|
|
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 `_plantCareInfoStorage` lazy service
|
|
- Add `plantCareInfoRepository` accessor
|
|
- Update `_fetchPlantCareUseCase` to inject repository
|
|
- Add to `resetAll()` method
|
|
|
|
### Step 7: Update PlantMO
|
|
**File:** `PlantGuide/Data/DataSources/Local/CoreData/ManagedObjects/PlantMO.swift`
|
|
|
|
Add relationship property:
|
|
```swift
|
|
@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)
|
|
- `shouldMigrateStoreAutomatically` and `shouldInferMappingModelAutomatically` already 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
|
|
|
|
1. **Build verification:** `xcodebuild -scheme PlantGuide build`
|
|
|
|
2. **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
|
|
|
|
3. **Test timing preservation:**
|
|
- Verify watering frequency `intervalDays` property works after cache retrieval
|
|
- Create care schedule from cached info → verify notifications scheduled correctly
|
|
|
|
4. **Test cache expiration:**
|
|
- Manually set `fetchedAt` to 8 days ago
|
|
- View plant details → should re-fetch from API
|
|
|
|
5. **Run existing tests:** `xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 17'`
|