- 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>
10 KiB
10 KiB
Botanica - Plant Identification iOS App
Overview
Custom iOS 17+ app using SwiftUI that identifies plants via camera with care schedules.
Stack:
- On-device ML: PlantNet-300K model converted to Core ML
- Online API: Pl@ntNet (my.plantnet.org) for higher accuracy
- Care data: Trefle API (open source botanical database)
- Architecture: Clean Architecture + MVVM
Phase 1: Foundation (Week 1-2)
Goal: Core infrastructure with camera capture
| Task | Description |
|---|---|
| 1.1 | Create Xcode project (iOS 17+, SwiftUI) |
| 1.2 | Set up folder structure (App/, Core/, Domain/, Data/, ML/, Presentation/) |
| 1.3 | Implement DIContainer.swift for dependency injection |
| 1.4 | Create domain entities: Plant, PlantIdentification, PlantCareSchedule, CareTask |
| 1.5 | Define repository protocols in Domain/RepositoryInterfaces/ |
| 1.6 | Build NetworkService.swift with async/await and multipart upload |
| 1.7 | Implement CameraView + CameraViewModel with AVFoundation |
| 1.8 | Set up Core Data stack for persistence |
| 1.9 | Create tab navigation (Camera, Collection, Care, Settings) |
Deliverable: Working camera capture with photo preview
Phase 2: On-Device ML (Week 3-4)
Goal: Offline plant identification with Core ML
| Task | Description |
|---|---|
| 2.1 | Download PlantNet-300K pre-trained ResNet weights |
| 2.2 | Convert to Core ML using coremltools (Python script) |
| 2.3 | Add PlantNet300K.mlpackage to Xcode |
| 2.4 | Create PlantLabels.json with 1,081 species names |
| 2.5 | Implement PlantClassificationService.swift using Vision framework |
| 2.6 | Create ImagePreprocessor.swift for model input normalization |
| 2.7 | Build IdentifyPlantOnDeviceUseCase.swift |
| 2.8 | Create IdentificationView showing results with confidence scores |
| 2.9 | Build SpeciesMatchCard and ConfidenceIndicator components |
| 2.10 | Performance test on device (target: <500ms inference) |
Deliverable: End-to-end offline identification flow
Phase 3: PlantNet API Integration (Week 5-6)
Goal: Hybrid identification with API fallback
| Task | Description |
|---|---|
| 3.1 | Register at my.plantnet.org for API key |
| 3.2 | Create PlantNetEndpoints.swift (POST /v2/identify/{project}) |
| 3.3 | Implement PlantNetAPIService.swift with multipart image upload |
| 3.4 | Create DTOs: PlantNetIdentifyResponseDTO, PlantNetSpeciesDTO |
| 3.5 | Build PlantNetMapper.swift (DTO → Domain entity) |
| 3.6 | Implement IdentifyPlantOnlineUseCase.swift |
| 3.7 | Create HybridIdentificationUseCase.swift (on-device first, API for confirmation) |
| 3.8 | Add network reachability monitoring |
| 3.9 | Handle rate limiting (500 free requests/day) |
| 3.10 | Implement IdentificationCache.swift for previous results |
Deliverable: Hybrid identification combining on-device + API
Phase 4: Trefle API & Plant Care (Week 7-8)
Goal: Complete care information and scheduling
| Task | Description |
|---|---|
| 4.1 | Register at trefle.io for API token |
| 4.2 | Create TrefleEndpoints.swift (GET /plants/search, GET /species/{slug}) |
| 4.3 | Implement TrefleAPIService.swift |
| 4.4 | Create DTOs: TrefleSpeciesDTO, GrowthDataDTO |
| 4.5 | Build TrefleMapper.swift mapping growth data to care schedules |
| 4.6 | Implement FetchPlantCareUseCase.swift |
| 4.7 | Create CreateCareScheduleUseCase.swift |
| 4.8 | Build PlantDetailView with CareInformationSection |
| 4.9 | Implement CareScheduleView with upcoming tasks |
| 4.10 | Add local notifications for care reminders |
Deliverable: Full plant care data with watering/fertilizer schedules
Phase 5: Plant Collection & Persistence (Week 9-10)
Goal: Saved plants with full offline support
| Task | Description |
|---|---|
| 5.1 | Define Core Data models (PlantMO, CareScheduleMO, IdentificationMO) |
| 5.2 | Implement CoreDataPlantStorage.swift |
| 5.3 | Build PlantCollectionRepository.swift |
| 5.4 | Create use cases: SavePlantUseCase, FetchCollectionUseCase |
| 5.5 | Build CollectionView with grid layout |
| 5.6 | Implement ImageCache.swift for offline images |
| 5.7 | Add search/filter in collection |
Deliverable: Full plant collection management with offline support
Phase 6: Polish & Release (Week 11-12)
Goal: Production-ready application
| Task | Description |
|---|---|
| 6.1 | Build SettingsView (offline mode toggle, API status, cache clear) |
| 6.2 | Add comprehensive error handling with ErrorView |
| 6.3 | Implement loading states with shimmer effects |
| 6.4 | Add accessibility labels and Dynamic Type support |
| 6.5 | Performance optimization pass |
| 6.6 | Write unit tests for use cases and services |
| 6.7 | Write UI tests for critical flows |
| 6.8 | Final QA and bug fixes |
Deliverable: App Store ready application
Project Structure
Botanica/
├── App/
│ ├── BotanicaApp.swift
│ └── Configuration/
│ ├── AppConfiguration.swift
│ └── APIKeys.swift
├── Core/
│ ├── DI/DIContainer.swift
│ ├── Extensions/
│ └── Utilities/
├── Domain/
│ ├── Entities/
│ │ ├── Plant.swift
│ │ ├── PlantIdentification.swift
│ │ ├── PlantCareSchedule.swift
│ │ └── CareTask.swift
│ ├── UseCases/
│ │ ├── Identification/
│ │ │ ├── IdentifyPlantOnDeviceUseCase.swift
│ │ │ ├── IdentifyPlantOnlineUseCase.swift
│ │ │ └── HybridIdentificationUseCase.swift
│ │ ├── PlantCare/
│ │ │ ├── FetchPlantCareUseCase.swift
│ │ │ └── CreateCareScheduleUseCase.swift
│ │ └── Collection/
│ └── RepositoryInterfaces/
├── Data/
│ ├── Repositories/
│ ├── DataSources/
│ │ ├── Remote/
│ │ │ ├── PlantNetAPI/
│ │ │ │ ├── PlantNetAPIService.swift
│ │ │ │ └── DTOs/
│ │ │ ├── TrefleAPI/
│ │ │ │ ├── TrefleAPIService.swift
│ │ │ │ └── DTOs/
│ │ │ └── NetworkService/
│ │ └── Local/
│ │ ├── CoreData/
│ │ └── Cache/
│ └── Mappers/
├── ML/
│ ├── Models/
│ │ └── PlantNet300K.mlpackage
│ ├── Services/
│ │ └── PlantClassificationService.swift
│ └── Preprocessing/
├── Presentation/
│ ├── Scenes/
│ │ ├── Camera/
│ │ ├── Identification/
│ │ ├── PlantDetail/
│ │ ├── Collection/
│ │ ├── CareSchedule/
│ │ └── Settings/
│ ├── Common/Components/
│ └── Navigation/
└── Resources/
├── PlantLabels.json
└── Assets.xcassets
API Details
Pl@ntNet API
- Base URL:
https://my-api.plantnet.org - Endpoint:
POST /v2/identify/{project} - Free tier: 500 requests/day
- Coverage: 77,565 species
- Documentation: my.plantnet.org/doc
Trefle API
- Base URL:
https://trefle.io/api/v1 - Endpoints:
GET /plants/search?q={name}GET /species/{slug}
- Data: Light requirements, watering, soil, temperature, fertilizer, growth info
- Documentation: docs.trefle.io
Core ML Conversion
Prerequisites
pip install torch torchvision coremltools pillow numpy
Conversion Script
# scripts/convert_plantnet_to_coreml.py
import torch
import torchvision.models as models
import coremltools as ct
# Load PlantNet-300K pre-trained ResNet
model = models.resnet50(weights=None)
model.fc = torch.nn.Linear(model.fc.in_features, 1081)
model.load_state_dict(torch.load("resnet50_weights_best_acc.tar", map_location='cpu')['state_dict'])
model.eval()
# Trace for conversion
traced = torch.jit.trace(model, torch.rand(1, 3, 224, 224))
# Convert to Core ML
image_input = ct.ImageType(
name="image",
shape=(1, 3, 224, 224),
scale=1/255.0,
bias=[-0.485/0.229, -0.456/0.224, -0.406/0.225],
color_layout=ct.colorlayout.RGB
)
mlmodel = ct.convert(
traced,
inputs=[image_input],
convert_to="mlprogram",
minimum_deployment_target=ct.target.iOS17,
compute_precision=ct.precision.FLOAT16,
)
mlmodel.save("PlantNet300K.mlpackage")
Download Weights
# From Zenodo (PlantNet-300K official)
wget https://zenodo.org/records/4726653/files/resnet50_weights_best_acc.tar
Key Data Models
Plant Entity
struct Plant: Identifiable, Sendable {
let id: UUID
let scientificName: String
let commonNames: [String]
let family: String
let genus: String
let imageURLs: [URL]
let dateIdentified: Date
let identificationSource: IdentificationSource
enum IdentificationSource: String {
case onDevice, plantNetAPI, hybrid
}
}
PlantCareSchedule Entity
struct PlantCareSchedule: Identifiable, Sendable {
let id: UUID
let plantID: UUID
let lightRequirement: LightRequirement
let wateringSchedule: WateringSchedule
let temperatureRange: TemperatureRange
let fertilizerSchedule: FertilizerSchedule?
let tasks: [CareTask]
}
Verification Checklist
| Test | Expected Result |
|---|---|
| Camera capture | Take photo → preview displays |
| On-device ML | Photo → top 10 species with confidence scores (<500ms) |
| PlantNet API | Photo → API results match/exceed on-device accuracy |
| Trefle API | Scientific name → care data (watering, light, fertilizer) |
| Save plant | Save to collection → persists after app restart |
| Offline mode | Disable network → on-device identification still works |
| Care reminders | Create schedule → notification fires at scheduled time |