- 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>
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
LocalPlantEntryCodable 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
PlantCategoryenum with 11 cases matching JSON categories - Create
LocalPlantDatabaseCodable 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
PlantDatabaseServiceactor 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
PlantDatabaseErrorenum:fileNotFounddecodingFailed(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.jsontoPlantGuide/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
LookupPlantUseCaseProtocolvia 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
BrowsePlantsViewwith:- 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
LocalPlantRowcomponent 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.fillorleaf.fill - Label: "Browse"
- Icon:
- Update tab order: Camera → Browse → Collection → Care → Settings
- Wire up
BrowsePlantsViewwith 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
PlantDatabaseServiceas singleton (load once, reuse) - Register
LookupPlantUseCasewith database service dependency - Register
BrowsePlantsViewModelfactory - 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
actorforPlantDatabaseServiceto 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
- Air Plant: