- 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>
342 lines
12 KiB
Markdown
342 lines
12 KiB
Markdown
# 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`
|