Files
PlantGuide/Docs/Phase1_plan.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

12 KiB

Phase 1: Foundation + Local Plant Database

Goal: Core infrastructure with camera capture and local plant database integration using houseplants_list.json (2,278 plants, 11 categories, 50 families)


Data Source Overview

File: data/houseplants_list.json

  • Total Plants: 2,278
  • Categories: Air Plant, Bromeliad, Cactus, Fern, Flowering Houseplant, Herb, Orchid, Palm, Succulent, Trailing/Climbing, Tropical Foliage
  • Families: 50 unique botanical families
  • Structure per plant:
    {
      "scientific_name": "Philodendron hederaceum",
      "common_names": ["Heartleaf Philodendron", "Sweetheart Plant"],
      "family": "Araceae",
      "category": "Tropical Foliage"
    }
    

Tasks

1.1 Create Local Plant Database Model

File: PlantGuide/Data/DataSources/Local/PlantDatabase/LocalPlantEntry.swift

  • Create LocalPlantEntry Codable struct matching JSON structure:
    struct LocalPlantEntry: Codable, Identifiable, Sendable {
        let scientificName: String
        let commonNames: [String]
        let family: String
        let category: PlantCategory
    
        var id: String { scientificName }
    
        enum CodingKeys: String, CodingKey {
            case scientificName = "scientific_name"
            case commonNames = "common_names"
            case family
            case category
        }
    }
    
  • Create PlantCategory enum with 11 cases matching JSON categories
  • Create LocalPlantDatabase Codable wrapper:
    struct LocalPlantDatabase: Codable, Sendable {
        let sourceDate: String
        let totalPlants: Int
        let sources: [String]
        let plants: [LocalPlantEntry]
    
        enum CodingKeys: String, CodingKey {
            case sourceDate = "source_date"
            case totalPlants = "total_plants"
            case sources, plants
        }
    }
    

Acceptance Criteria: Models compile and can decode houseplants_list.json without errors


1.2 Implement Plant Database Service

File: PlantGuide/Data/DataSources/Local/PlantDatabase/PlantDatabaseService.swift

  • Create PlantDatabaseServiceProtocol:
    protocol PlantDatabaseServiceProtocol: Sendable {
        func loadDatabase() async throws
        func searchByScientificName(_ query: String) async -> [LocalPlantEntry]
        func searchByCommonName(_ query: String) async -> [LocalPlantEntry]
        func searchAll(_ query: String) async -> [LocalPlantEntry]
        func getByFamily(_ family: String) async -> [LocalPlantEntry]
        func getByCategory(_ category: PlantCategory) async -> [LocalPlantEntry]
        func getPlant(scientificName: String) async -> LocalPlantEntry?
        var allCategories: [PlantCategory] { get }
        var allFamilies: [String] { get }
        var plantCount: Int { get }
    }
    
  • Implement PlantDatabaseService actor for thread safety:
    • Load JSON from bundle on first access
    • Build search indices for fast lookups
    • Implement fuzzy matching for search (handle typos)
    • Cache loaded database in memory
  • Create PlantDatabaseError enum:
    • fileNotFound
    • decodingFailed(Error)
    • notLoaded

Acceptance Criteria:

  • Service loads all 2,278 plants without memory issues
  • Search returns results in < 50ms for any query
  • Case-insensitive search works for scientific and common names

1.3 Add JSON to Xcode Bundle

  • Copy data/houseplants_list.json to PlantGuide/Resources/ folder
  • Add file to Xcode project target (ensure "Copy Bundle Resources" includes it)
  • Verify file accessible via Bundle.main.url(forResource:withExtension:)

Acceptance Criteria: Bundle.main.url(forResource: "houseplants_list", withExtension: "json") returns valid URL


1.4 Create Plant Lookup Use Case

File: PlantGuide/Domain/UseCases/PlantLookup/LookupPlantUseCase.swift

  • Create LookupPlantUseCase:
    protocol LookupPlantUseCaseProtocol: Sendable {
        func execute(scientificName: String) async -> LocalPlantEntry?
        func search(query: String) async -> [LocalPlantEntry]
        func suggestMatches(for identifiedName: String, confidence: Double) async -> [LocalPlantEntry]
    }
    
  • Implement suggestion logic:
    • If confidence < 0.7, return top 5 fuzzy matches from local database
    • If confidence >= 0.7, return exact match + similar species from same genus
  • Handle cultivar names (e.g., 'Brasil', 'Pink Princess') by matching base species

Acceptance Criteria:

  • suggestMatches(for: "Philodendron hederaceum", confidence: 0.9) returns the plant + related cultivars
  • Fuzzy search for "Philo brasil" finds "Philodendron hederaceum 'Brasil'"

1.5 Integrate with Identification Flow

File: PlantGuide/Presentation/Scenes/Identification/IdentificationViewModel.swift

  • Inject LookupPlantUseCaseProtocol via DI container
  • After ML identification, look up plant in local database:
    • Enrich results with category and family data
    • Show "Found in local database" badge for verified matches
    • Display related species suggestions for low-confidence identifications
  • Add localDatabaseMatch: LocalPlantEntry? property to view model state

Acceptance Criteria:

  • Identification results show category (e.g., "Tropical Foliage") from local database
  • Low-confidence results display "Did you mean..." suggestions from local database

1.6 Create Plant Browse View

File: PlantGuide/Presentation/Scenes/Browse/BrowsePlantsView.swift

  • Create BrowsePlantsView with:
    • Category filter chips (11 categories)
    • Search bar for name lookup
    • Alphabetical section list grouped by first letter
    • Plant count badge showing total matches
  • Create BrowsePlantsViewModel:
    @MainActor
    final class BrowsePlantsViewModel: ObservableObject {
        @Published var searchQuery = ""
        @Published var selectedCategory: PlantCategory?
        @Published var plants: [LocalPlantEntry] = []
        @Published var isLoading = false
    
        func loadPlants() async
        func search() async
        func filterByCategory(_ category: PlantCategory?) async
    }
    
  • Create LocalPlantRow component showing:
    • Scientific name (primary)
    • Common names (secondary, comma-separated)
    • Family badge
    • Category icon

Acceptance Criteria:

  • Browse view displays all 2,278 plants with smooth scrolling
  • Category filter correctly shows only plants in selected category
  • Search finds plants by any name (scientific or common)

1.7 Add Browse Tab to Navigation

File: PlantGuide/Presentation/Navigation/MainTabView.swift

  • Add "Browse" tab between Camera and Collection:
    • Icon: book.fill or leaf.fill
    • Label: "Browse"
  • Update tab order: Camera → Browse → Collection → Care → Settings
  • Wire up BrowsePlantsView with DI container dependencies

Acceptance Criteria: Browse tab displays and switches correctly, shows plant database


1.8 Update DI Container

File: PlantGuide/Core/DI/DIContainer.swift

  • Register PlantDatabaseService as singleton (load once, reuse)
  • Register LookupPlantUseCase with database service dependency
  • Register BrowsePlantsViewModel factory
  • Add lazy initialization for database service (load on first access, not app launch)

Acceptance Criteria: All new dependencies resolve correctly without circular references


1.9 Create Local Database Tests

File: PlantGuideTests/Data/PlantDatabaseServiceTests.swift

  • Test JSON loading success
  • Test plant count equals 2,278
  • Test category count equals 11
  • Test family count equals 50
  • Test search by scientific name (exact match)
  • Test search by common name (partial match)
  • Test case-insensitive search
  • Test category filter returns only plants in category
  • Test empty search returns empty array
  • Test cultivar name matching (e.g., searching "Pink Princess" finds Philodendron erubescens 'Pink Princess')

Acceptance Criteria: All tests pass, code coverage > 80% for PlantDatabaseService


End-of-Phase Validation

Functional Verification

Test Steps Expected Result Status
Database Load Launch app, go to Browse tab Plants display without crash, count shows 2,278 [ ]
Category Filter Select "Cactus" category Only cactus plants shown, count updates [ ]
Search Scientific Search "Monstera deliciosa" Exact match appears at top [ ]
Search Common Search "Snake Plant" Sansevieria varieties appear [ ]
Search Partial Search "philo" All Philodendron species appear [ ]
Identification Enrichment Identify a plant via camera Category and family from local DB shown in results [ ]
Low Confidence Suggestions Get low-confidence identification "Did you mean..." suggestions appear from local DB [ ]
Scroll Performance Scroll through all plants quickly No dropped frames, smooth 60fps [ ]
Memory Usage Load database, navigate away, return Memory stable, no leaks [ ]

Code Quality Verification

Check Criteria Status
Build Project builds with zero warnings [ ]
Tests All PlantDatabaseService tests pass [ ]
Coverage Code coverage > 80% for new code [ ]
Sendable All new types conform to Sendable [ ]
Actor Isolation PlantDatabaseService is thread-safe actor [ ]
Error Handling All async functions have proper try/catch [ ]
Accessibility Browse view has accessibility labels [ ]

Performance Verification

Metric Target Status
Database Load < 500ms first load [ ]
Search Response < 50ms per query [ ]
Memory (Browse) < 30 MB additional [ ]
Scroll FPS 60 fps constant [ ]
App Launch Impact < 100ms added to launch [ ]

Phase 1 Completion Checklist

  • All 9 tasks completed
  • All functional tests pass
  • All code quality checks pass
  • All performance targets met
  • Unit tests written and passing
  • Code committed with descriptive message
  • Ready for Phase 2 (On-Device ML Integration)

File Manifest

New files to create:

PlantGuide/
├── Data/
│   └── DataSources/
│       └── Local/
│           └── PlantDatabase/
│               ├── LocalPlantEntry.swift
│               ├── LocalPlantDatabase.swift
│               ├── PlantCategory.swift
│               ├── PlantDatabaseService.swift
│               └── PlantDatabaseError.swift
├── Domain/
│   └── UseCases/
│       └── PlantLookup/
│           └── LookupPlantUseCase.swift
├── Presentation/
│   └── Scenes/
│       └── Browse/
│           ├── BrowsePlantsView.swift
│           ├── BrowsePlantsViewModel.swift
│           └── Components/
│               └── LocalPlantRow.swift
└── Resources/
    └── houseplants_list.json (copied from data/)

PlantGuideTests/
└── Data/
    └── PlantDatabaseServiceTests.swift

Files to modify:

PlantGuide/
├── Core/DI/DIContainer.swift (add new registrations)
├── Presentation/Navigation/MainTabView.swift (add Browse tab)
└── Presentation/Scenes/Identification/IdentificationViewModel.swift (add local lookup)

Notes

  • Use actor for PlantDatabaseService to ensure thread safety for concurrent searches
  • Consider implementing Trie data structure for fast prefix-based search if needed
  • The JSON should be loaded lazily on first browse access, not at app launch
  • For cultivar matching, strip quotes and match base species name
  • Category icons suggestion:
    • Air Plant: leaf.arrow.triangle.circlepath
    • Bromeliad: sparkles
    • Cactus: sun.max.fill
    • Fern: leaf.fill
    • Flowering Houseplant: camera.macro
    • Herb: leaf.circle
    • Orchid: camera.macro.circle
    • Palm: tree.fill
    • Succulent: drop.fill
    • Trailing/Climbing: arrow.up.right
    • Tropical Foliage: leaf.fill