Files
PlantGuide/Docs/save_shit_implementation.md
Trey t 136dfbae33 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>
2026-01-23 12:18:01 -06:00

11 KiB

Implementation Plan: Persist PlantCareInfo in Core Data

Overview

Cache Trefle API care info locally so the API is only called once per plant. This preserves all timing info (watering frequency, fertilizer schedule) for proper notification scheduling.

Goal: Reduce unnecessary API calls, improve offline experience, preserve care timing data

Estimated Complexity: Medium Risk Level: Low (lightweight migration, optional relationships)


Phase 1: Value Transformers

Add JSON-based transformers for complex types that need Core Data persistence.

Tasks

Task File Description
1.1 Core/Utilities/ValueTransformers.swift Add WateringScheduleTransformer - encodes WateringSchedule struct to JSON Data
1.2 Core/Utilities/ValueTransformers.swift Add TemperatureRangeTransformer - encodes TemperatureRange struct to JSON Data
1.3 Core/Utilities/ValueTransformers.swift Add FertilizerScheduleTransformer - encodes FertilizerSchedule struct to JSON Data
1.4 Core/Utilities/ValueTransformers.swift Add SeasonArrayTransformer - encodes [Season] array to JSON Data
1.5 App/PlantGuideApp.swift Register all 4 new transformers in app init

Acceptance Criteria

  • All transformers follow existing IdentificationResultArrayTransformer pattern
  • Transformers handle nil values gracefully
  • Round-trip encoding/decoding preserves all data
  • Build succeeds with no warnings

Phase 2: Core Data Model Update

Add the PlantCareInfoMO entity and relationship to PlantMO.

Tasks

Task File Description
2.1 PlantGuideModel.xcdatamodeld Create new PlantCareInfoMO entity with all attributes (see schema below)
2.2 PlantGuideModel.xcdatamodeld Add plant relationship from PlantCareInfoMO to PlantMO (optional, one-to-one)
2.3 PlantGuideModel.xcdatamodeld Add plantCareInfo relationship from PlantMO to PlantCareInfoMO (optional, cascade delete)
2.4 ManagedObjects/PlantMO.swift Add @NSManaged public var plantCareInfo: PlantCareInfoMO? property

PlantCareInfoMO Schema

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

Acceptance Criteria

  • Entity created with all attributes correctly typed
  • Relationships defined with proper inverse relationships
  • Cascade delete rule set on PlantMO side
  • Build succeeds - lightweight migration should auto-apply

Phase 3: Managed Object Implementation

Create the PlantCareInfoMO managed object class with domain mapping.

Tasks

Task File Description
3.1 ManagedObjects/PlantCareInfoMO.swift Create new file with @NSManaged properties
3.2 ManagedObjects/PlantCareInfoMO.swift Implement toDomainModel() -> PlantCareInfo? - decodes JSON data to domain structs
3.3 ManagedObjects/PlantCareInfoMO.swift Implement static func fromDomainModel(_:context:) -> PlantCareInfoMO? - encodes domain to MO
3.4 ManagedObjects/PlantCareInfoMO.swift Implement func update(from: PlantCareInfo) - updates existing MO from domain model

Acceptance Criteria

  • All @NSManaged properties defined
  • toDomainModel() handles all optional fields correctly
  • fromDomainModel() creates valid managed object
  • update(from:) preserves relationships
  • JSON encoding/decoding uses transformers correctly

Phase 4: Repository Layer

Create the repository protocol and Core Data implementation.

Tasks

Task File Description
4.1 Domain/RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift Create protocol with fetch, save, delete, and cache staleness methods
4.2 Data/DataSources/Local/CoreData/CoreDataPlantCareInfoStorage.swift Create implementation conforming to protocol
4.3 CoreDataPlantCareInfoStorage.swift Implement fetch(scientificName:) with predicate query
4.4 CoreDataPlantCareInfoStorage.swift Implement fetch(trefleID:) with predicate query
4.5 CoreDataPlantCareInfoStorage.swift Implement fetch(for plantID:) via relationship
4.6 CoreDataPlantCareInfoStorage.swift Implement save(_:for:) - creates or updates MO
4.7 CoreDataPlantCareInfoStorage.swift Implement isCacheStale(scientificName:cacheExpiration:) - checks fetchedAt date
4.8 CoreDataPlantCareInfoStorage.swift Implement delete(for plantID:) - removes cache entry

Protocol Definition

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
}

Acceptance Criteria

  • Protocol is Sendable for Swift concurrency
  • All fetch methods return optional (nil if not found)
  • Save method handles both create and update cases
  • Cache staleness uses 7-day default expiration
  • Delete method handles nil relationship gracefully

Phase 5: Use Case Integration

Update FetchPlantCareUseCase with cache-first logic.

Tasks

Task File Description
5.1 UseCases/PlantCare/FetchPlantCareUseCase.swift Inject PlantCareInfoRepositoryProtocol dependency
5.2 UseCases/PlantCare/FetchPlantCareUseCase.swift Add cache check at start of execute()
5.3 UseCases/PlantCare/FetchPlantCareUseCase.swift Add cache staleness validation (7-day expiration)
5.4 UseCases/PlantCare/FetchPlantCareUseCase.swift Save API response to cache after successful fetch
5.5 UseCases/PlantCare/FetchPlantCareUseCase.swift Handle cache errors gracefully (fall back to API)

Updated Execute Logic

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
}

Acceptance Criteria

  • Cache hit returns immediately without API call
  • Stale cache triggers fresh API fetch
  • API response is saved to cache
  • Cache errors don't block API fallback
  • Timing info (watering interval) preserved in cache

Phase 6: Dependency Injection

Wire up all new components in DIContainer.

Tasks

Task File Description
6.1 Core/DI/DIContainer.swift Add _plantCareInfoStorage lazy property
6.2 Core/DI/DIContainer.swift Add plantCareInfoRepository computed accessor
6.3 Core/DI/DIContainer.swift Update _fetchPlantCareUseCase to inject repository
6.4 Core/DI/DIContainer.swift Add storage to resetAll() method for testing

Acceptance Criteria

  • Storage is lazy-initialized
  • Repository accessor returns protocol type
  • Use case receives repository dependency
  • Reset clears cache for testing

Phase 7: Verification

Validate the implementation works correctly.

Tasks

Task Type Description
7.1 Build Run xcodebuild -scheme PlantGuide build - verify zero errors
7.2 Manual Test Add new plant -> view details (should call Trefle API)
7.3 Manual Test Navigate away and back to details (should NOT call API)
7.4 Manual Test Verify watering intervalDays property works after cache retrieval
7.5 Manual Test Create care schedule from cached info -> verify notifications
7.6 Cache Expiration Manually set fetchedAt to 8 days ago -> should re-fetch
7.7 Unit Tests Run xcodebuild test -scheme PlantGuide

Acceptance Criteria

  • Build succeeds with zero warnings
  • API only called once per plant (check console logs)
  • Cached care info identical to API response
  • Care timing preserved for notification scheduling
  • Cache expiration triggers refresh after 7 days
  • All existing tests pass

Files Summary

File Action
Core/Utilities/ValueTransformers.swift MODIFY - Add 4 transformers
PlantGuideModel.xcdatamodeld MODIFY - Add PlantCareInfoMO entity
ManagedObjects/PlantCareInfoMO.swift CREATE - Managed object + mappers
ManagedObjects/PlantMO.swift MODIFY - Add relationship
Domain/RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift CREATE - Protocol
Data/DataSources/Local/CoreData/CoreDataPlantCareInfoStorage.swift CREATE - Implementation
Domain/UseCases/PlantCare/FetchPlantCareUseCase.swift MODIFY - Add cache-first logic
Core/DI/DIContainer.swift MODIFY - Register dependencies
App/PlantGuideApp.swift MODIFY - Register transformers

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

Risk Assessment

Risk Likelihood Impact Mitigation
Migration failure Low High Lightweight migration; optional relationships
Cache corruption Low Medium JSON encoding is deterministic; handle decode failures gracefully
Stale cache served Low Low 7-day expiration; manual refresh available
Memory pressure Low Low Cache is per-plant, not bulk loaded

Notes

  • Follow existing patterns in IdentificationResultArrayTransformer for transformers
  • Use @NSManaged properties pattern from existing MO classes
  • Repository pattern matches existing PlantCollectionRepositoryProtocol
  • Cache expiration of 7 days balances freshness vs API calls
  • Cascade delete ensures orphan cleanup