Add PlantGuide iOS app with plant identification and care management

- 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>
This commit is contained in:
Trey t
2026-01-23 12:18:01 -06:00
parent d3ab29eb84
commit 136dfbae33
187 changed files with 69001 additions and 0 deletions

167
Docs/save_shit.md Normal file
View File

@@ -0,0 +1,167 @@
# 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'`