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:
341
Docs/Phase1_plan.md
Normal file
341
Docs/Phase1_plan.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# 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:**
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```swift
|
||||
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:
|
||||
```swift
|
||||
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`:
|
||||
```swift
|
||||
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`:
|
||||
```swift
|
||||
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`:
|
||||
```swift
|
||||
@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`
|
||||
Reference in New Issue
Block a user