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

32 KiB

Phase 4: Trefle API & Plant Care

Goal: Complete care information and scheduling with local notifications

Prerequisites: Phase 3 complete (hybrid identification working, API infrastructure established)


Tasks

4.1 Register for Trefle API Access

  • Navigate to trefle.io
  • Create developer account
  • Generate API token
  • Review API documentation and rate limits
  • Add TREFLE_API_TOKEN to APIKeys.swift:
    enum APIKeys {
        // ... existing keys
    
        static let trefleAPIToken: String = {
            guard let token = Bundle.main.object(forInfoDictionaryKey: "TREFLE_API_TOKEN") as? String else {
                fatalError("Trefle API token not configured")
            }
            return token
        }()
    }
    
  • Add TREFLE_API_TOKEN to Info.plist via xcconfig
  • Update .xcconfig file with Trefle token (already in .gitignore)
  • Verify API access with test request

Acceptance Criteria: API token configured and accessible, test request returns valid data


4.2 Create Trefle Endpoints

  • Create Data/DataSources/Remote/TrefleAPI/TrefleEndpoints.swift
  • Define endpoint configuration:
    enum TrefleEndpoint: Endpoint {
        case searchPlants(query: String, page: Int)
        case getSpecies(slug: String)
        case getSpeciesById(id: Int)
        case getPlant(id: Int)
    
        var baseURL: URL { URL(string: "https://trefle.io/api/v1")! }
    
        var path: String {
            switch self {
            case .searchPlants: return "/plants/search"
            case .getSpecies(let slug): return "/species/\(slug)"
            case .getSpeciesById(let id): return "/species/\(id)"
            case .getPlant(let id): return "/plants/\(id)"
            }
        }
    
        var method: HTTPMethod { .get }
    
        var queryItems: [URLQueryItem] {
            var items = [URLQueryItem(name: "token", value: APIKeys.trefleAPIToken)]
            switch self {
            case .searchPlants(let query, let page):
                items.append(URLQueryItem(name: "q", value: query))
                items.append(URLQueryItem(name: "page", value: String(page)))
            default:
                break
            }
            return items
        }
    }
    
  • Support pagination for search results
  • Add filter parameters (edible, vegetable, etc.)

Acceptance Criteria: Endpoints build correct URLs with token and query parameters


4.3 Implement Trefle API Service

  • Create Data/DataSources/Remote/TrefleAPI/TrefleAPIService.swift
  • Define protocol:
    protocol TrefleAPIServiceProtocol: Sendable {
        func searchPlants(query: String, page: Int) async throws -> TrefleSearchResponseDTO
        func getSpecies(slug: String) async throws -> TrefleSpeciesResponseDTO
        func getSpeciesById(id: Int) async throws -> TrefleSpeciesResponseDTO
    }
    
  • Implement service using NetworkService:
    • Handle token-based authentication
    • Parse paginated responses
    • Handle 404 for unknown species
  • Implement retry logic (1 retry with exponential backoff)
  • Add request timeout (15 seconds)
  • Handle rate limiting (120 requests/minute)
  • Log request/response for debugging

Acceptance Criteria: Service retrieves species data and handles errors gracefully


4.4 Create Trefle DTOs

  • Create Data/DataSources/Remote/TrefleAPI/DTOs/TrefleDTOs.swift
  • Define response DTOs:
    struct TrefleSearchResponseDTO: Decodable {
        let data: [TreflePlantSummaryDTO]
        let links: TrefleLinksDTO
        let meta: TrefleMetaDTO
    }
    
    struct TrefleSpeciesResponseDTO: Decodable {
        let data: TrefleSpeciesDTO
        let meta: TrefleMetaDTO
    }
    
  • Create TrefleSpeciesDTO:
    struct TrefleSpeciesDTO: Decodable {
        let id: Int
        let commonName: String?
        let slug: String
        let scientificName: String
        let year: Int?
        let bibliography: String?
        let author: String?
        let familyCommonName: String?
        let family: String?
        let genus: String?
        let genusId: Int?
        let imageUrl: String?
        let images: TrefleImagesDTO?
        let distribution: TrefleDistributionDTO?
        let specifications: TrefleSpecificationsDTO?
        let growth: TrefleGrowthDTO?
        let synonyms: [TrefleSynonymDTO]?
        let sources: [TrefleSourceDTO]?
    }
    
  • Create TrefleGrowthDTO:
    struct TrefleGrowthDTO: Decodable {
        let description: String?
        let sowing: String?
        let daysToHarvest: Int?
        let rowSpacing: TrefleMeasurementDTO?
        let spread: TrefleMeasurementDTO?
        let phMaximum: Double?
        let phMinimum: Double?
        let light: Int?          // 0-10 scale
        let atmosphericHumidity: Int?  // 0-10 scale
        let growthMonths: [String]?
        let bloomMonths: [String]?
        let fruitMonths: [String]?
        let minimumPrecipitation: TrefleMeasurementDTO?
        let maximumPrecipitation: TrefleMeasurementDTO?
        let minimumRootDepth: TrefleMeasurementDTO?
        let minimumTemperature: TrefleMeasurementDTO?
        let maximumTemperature: TrefleMeasurementDTO?
        let soilNutriments: Int?  // 0-10 scale
        let soilSalinity: Int?    // 0-10 scale
        let soilTexture: Int?     // 0-10 scale
        let soilHumidity: Int?    // 0-10 scale
    }
    
  • Create supporting DTOs: TrefleSpecificationsDTO, TrefleImagesDTO, TrefleMeasurementDTO
  • Add CodingKeys for snake_case API responses
  • Write unit tests for DTO decoding

Acceptance Criteria: DTOs decode actual Trefle API responses without errors


4.5 Build Trefle Mapper

  • Create Data/Mappers/TrefleMapper.swift
  • Implement mapping functions:
    struct TrefleMapper {
        static func mapToPlantCareSchedule(
            from species: TrefleSpeciesDTO,
            plantID: UUID
        ) -> PlantCareSchedule
    
        static func mapToLightRequirement(
            from light: Int?
        ) -> LightRequirement
    
        static func mapToWateringSchedule(
            from growth: TrefleGrowthDTO?
        ) -> WateringSchedule
    
        static func mapToTemperatureRange(
            from growth: TrefleGrowthDTO?
        ) -> TemperatureRange
    
        static func mapToFertilizerSchedule(
            from growth: TrefleGrowthDTO?
        ) -> FertilizerSchedule?
    
        static func generateCareTasks(
            from schedule: PlantCareSchedule,
            startDate: Date
        ) -> [CareTask]
    }
    
  • Map Trefle light scale (0-10) to LightRequirement:
    enum LightRequirement: String, Codable, Sendable {
        case fullShade      // 0-2
        case partialShade   // 3-4
        case partialSun     // 5-6
        case fullSun        // 7-10
    
        var description: String { ... }
        var hoursOfLight: ClosedRange<Int> { ... }
    }
    
  • Map humidity/precipitation to WateringSchedule:
    struct WateringSchedule: Codable, Sendable {
        let frequency: WateringFrequency
        let amount: WateringAmount
        let seasonalAdjustments: [Season: WateringFrequency]?
    
        enum WateringFrequency: String, Codable, Sendable {
            case daily, everyOtherDay, twiceWeekly, weekly, biweekly, monthly
    
            var intervalDays: Int { ... }
        }
    
        enum WateringAmount: String, Codable, Sendable {
            case light, moderate, thorough, soak
        }
    }
    
  • Map temperature data to TemperatureRange:
    struct TemperatureRange: Codable, Sendable {
        let minimum: Measurement<UnitTemperature>
        let maximum: Measurement<UnitTemperature>
        let optimal: Measurement<UnitTemperature>?
        let frostTolerant: Bool
    }
    
  • Map soil nutrients to FertilizerSchedule:
    struct FertilizerSchedule: Codable, Sendable {
        let frequency: FertilizerFrequency
        let type: FertilizerType
        let seasonalApplication: Bool
        let activeMonths: [Int]?  // 1-12
    
        enum FertilizerFrequency: String, Codable, Sendable {
            case weekly, biweekly, monthly, quarterly, biannually
        }
    
        enum FertilizerType: String, Codable, Sendable {
            case balanced, highNitrogen, highPhosphorus, highPotassium, organic
        }
    }
    
  • Handle missing data with sensible defaults
  • Unit test all mapping functions

Acceptance Criteria: Mapper produces valid care schedules from all Trefle response variations


4.6 Implement Fetch Plant Care Use Case

  • Create Domain/UseCases/PlantCare/FetchPlantCareUseCase.swift
  • Define protocol:
    protocol FetchPlantCareUseCaseProtocol: Sendable {
        func execute(scientificName: String) async throws -> PlantCareInfo
        func execute(trefleId: Int) async throws -> PlantCareInfo
    }
    
  • Define PlantCareInfo domain entity:
    struct PlantCareInfo: Identifiable, Sendable {
        let id: UUID
        let scientificName: String
        let commonName: String?
        let lightRequirement: LightRequirement
        let wateringSchedule: WateringSchedule
        let temperatureRange: TemperatureRange
        let fertilizerSchedule: FertilizerSchedule?
        let soilType: SoilType?
        let humidity: HumidityLevel?
        let growthRate: GrowthRate?
        let bloomingSeason: [Season]?
        let additionalNotes: String?
        let sourceURL: URL?
    }
    
  • Implement use case:
    • Search Trefle by scientific name
    • Fetch detailed species data
    • Map to domain entity
    • Cache results for offline access
  • Handle species not found in Trefle
  • Add fallback to generic care data for unknown species
  • Register in DIContainer

Acceptance Criteria: Use case retrieves care data, handles missing species gracefully


4.7 Create Care Schedule Use Case

  • Create Domain/UseCases/PlantCare/CreateCareScheduleUseCase.swift
  • Define protocol:
    protocol CreateCareScheduleUseCaseProtocol: Sendable {
        func execute(
            for plant: Plant,
            careInfo: PlantCareInfo,
            userPreferences: CarePreferences?
        ) async throws -> PlantCareSchedule
    }
    
  • Define CarePreferences:
    struct CarePreferences: Codable, Sendable {
        let preferredWateringTime: DateComponents  // e.g., 8:00 AM
        let reminderDaysBefore: Int  // remind N days before task
        let groupWateringDays: Bool  // water all plants same day
        let adjustForSeason: Bool
        let location: PlantLocation?
    
        enum PlantLocation: String, Codable, Sendable {
            case indoor, outdoor, greenhouse, balcony
        }
    }
    
  • Implement schedule generation:
    • Calculate next N watering dates (30 days ahead)
    • Calculate fertilizer dates based on schedule
    • Adjust for seasons if enabled
    • Create CareTask entities for each scheduled item
  • Define CareTask entity:
    struct CareTask: Identifiable, Codable, Sendable {
        let id: UUID
        let plantID: UUID
        let type: CareTaskType
        let scheduledDate: Date
        let isCompleted: Bool
        let completedDate: Date?
        let notes: String?
    
        enum CareTaskType: String, Codable, Sendable {
            case watering, fertilizing, pruning, repotting, pestControl, rotation
    
            var icon: String { ... }
            var defaultReminderOffset: TimeInterval { ... }
        }
    }
    
  • Persist schedule to Core Data
  • Register in DIContainer

Acceptance Criteria: Use case creates complete care schedule with future tasks


4.8 Build Plant Detail View

  • Create Presentation/Scenes/PlantDetail/PlantDetailView.swift
  • Create PlantDetailViewModel:
    @Observable
    final class PlantDetailViewModel {
        private(set) var plant: Plant
        private(set) var careInfo: PlantCareInfo?
        private(set) var careSchedule: PlantCareSchedule?
        private(set) var isLoading: Bool = false
        private(set) var error: Error?
    
        func loadCareInfo() async
        func createSchedule(preferences: CarePreferences?) async
        func markTaskComplete(_ task: CareTask) async
    }
    
  • Implement view sections:
    struct PlantDetailView: View {
        @State private var viewModel: PlantDetailViewModel
    
        var body: some View {
            ScrollView {
                PlantHeaderSection(plant: viewModel.plant)
                IdentificationSection(plant: viewModel.plant)
                CareInformationSection(careInfo: viewModel.careInfo)
                UpcomingTasksSection(tasks: viewModel.upcomingTasks)
                CareScheduleSection(schedule: viewModel.careSchedule)
            }
        }
    }
    
  • Create CareInformationSection component:
    struct CareInformationSection: View {
        let careInfo: PlantCareInfo?
    
        var body: some View {
            Section("Care Requirements") {
                LightRequirementRow(requirement: careInfo?.lightRequirement)
                WateringRow(schedule: careInfo?.wateringSchedule)
                TemperatureRow(range: careInfo?.temperatureRange)
                FertilizerRow(schedule: careInfo?.fertilizerSchedule)
                HumidityRow(level: careInfo?.humidity)
            }
        }
    }
    
  • Create care info row components:
    • LightRequirementRow - sun icon, description, hours
    • WateringRow - drop icon, frequency, amount
    • TemperatureRow - thermometer, min/max/optimal
    • FertilizerRow - leaf icon, frequency, type
    • HumidityRow - humidity icon, level indicator
  • Add loading skeleton for care info
  • Handle "care data unavailable" state
  • Implement pull-to-refresh

Acceptance Criteria: Detail view displays all plant info with care requirements


4.9 Implement Care Schedule View

  • Create Presentation/Scenes/CareSchedule/CareScheduleView.swift
  • Create CareScheduleViewModel:
    @Observable
    final class CareScheduleViewModel {
        private(set) var upcomingTasks: [CareTask] = []
        private(set) var tasksByDate: [Date: [CareTask]] = [:]
        private(set) var plants: [Plant] = []
        var selectedFilter: TaskFilter = .all
    
        enum TaskFilter: CaseIterable {
            case all, watering, fertilizing, overdue, today
        }
    
        func loadTasks() async
        func markComplete(_ task: CareTask) async
        func snoozeTask(_ task: CareTask, until: Date) async
        func skipTask(_ task: CareTask) async
    }
    
  • Implement main schedule view:
    struct CareScheduleView: View {
        @State private var viewModel: CareScheduleViewModel
    
        var body: some View {
            NavigationStack {
                List {
                    OverdueTasksSection(tasks: viewModel.overdueTasks)
                    TodayTasksSection(tasks: viewModel.todayTasks)
                    UpcomingTasksSection(tasksByDate: viewModel.upcomingByDate)
                }
                .navigationTitle("Care Schedule")
                .toolbar {
                    FilterMenu(selection: $viewModel.selectedFilter)
                }
            }
        }
    }
    
  • Create CareTaskRow component:
    struct CareTaskRow: View {
        let task: CareTask
        let plant: Plant
        let onComplete: () -> Void
        let onSnooze: (Date) -> Void
    
        var body: some View {
            HStack {
                PlantThumbnail(plant: plant)
                VStack(alignment: .leading) {
                    Text(plant.commonNames.first ?? plant.scientificName)
                    Text(task.type.rawValue.capitalized)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Spacer()
                TaskActionButtons(...)
            }
            .swipeActions { ... }
        }
    }
    
  • Implement calendar view option:
    struct CareCalendarView: View {
        let tasksByDate: [Date: [CareTask]]
        @Binding var selectedDate: Date
    
        var body: some View {
            VStack {
                CalendarGrid(tasksByDate: tasksByDate, selection: $selectedDate)
                TaskListForDate(tasks: tasksByDate[selectedDate] ?? [])
            }
        }
    }
    
  • Add empty state for "no tasks scheduled"
  • Implement batch actions (complete all today's watering)
  • Add quick-add task functionality

Acceptance Criteria: Schedule view shows all upcoming tasks, supports filtering and completion


4.10 Add Local Notifications for Care Reminders

  • Create Core/Services/NotificationService.swift
  • Define protocol:
    protocol NotificationServiceProtocol: Sendable {
        func requestAuthorization() async throws -> Bool
        func scheduleReminder(for task: CareTask, plant: Plant) async throws
        func cancelReminder(for task: CareTask) async
        func cancelAllReminders(for plantID: UUID) async
        func updateBadgeCount() async
        func getPendingNotifications() async -> [UNNotificationRequest]
    }
    
  • Implement notification service:
    final class NotificationService: NotificationServiceProtocol {
        private let center = UNUserNotificationCenter.current()
    
        func scheduleReminder(for task: CareTask, plant: Plant) async throws {
            let content = UNMutableNotificationContent()
            content.title = "Plant Care Reminder"
            content.body = "\(plant.commonNames.first ?? plant.scientificName) needs \(task.type.rawValue)"
            content.sound = .default
            content.badge = await calculateBadgeCount() as NSNumber
            content.userInfo = [
                "taskID": task.id.uuidString,
                "plantID": plant.id.uuidString,
                "taskType": task.type.rawValue
            ]
            content.categoryIdentifier = "CARE_REMINDER"
    
            let trigger = UNCalendarNotificationTrigger(
                dateMatching: Calendar.current.dateComponents(
                    [.year, .month, .day, .hour, .minute],
                    from: task.scheduledDate
                ),
                repeats: false
            )
    
            let request = UNNotificationRequest(
                identifier: "care-\(task.id.uuidString)",
                content: content,
                trigger: trigger
            )
    
            try await center.add(request)
        }
    }
    
  • Set up notification categories and actions:
    func setupNotificationCategories() {
        let completeAction = UNNotificationAction(
            identifier: "COMPLETE",
            title: "Mark Complete",
            options: .foreground
        )
    
        let snoozeAction = UNNotificationAction(
            identifier: "SNOOZE",
            title: "Snooze 1 Hour",
            options: []
        )
    
        let category = UNNotificationCategory(
            identifier: "CARE_REMINDER",
            actions: [completeAction, snoozeAction],
            intentIdentifiers: [],
            options: .customDismissAction
        )
    
        UNUserNotificationCenter.current().setNotificationCategories([category])
    }
    
  • Handle notification responses in app delegate/scene delegate
  • Create ScheduleNotificationsUseCase:
    protocol ScheduleNotificationsUseCaseProtocol: Sendable {
        func scheduleAll(for schedule: PlantCareSchedule, plant: Plant) async throws
        func rescheduleAll() async throws  // Call after task completion
        func syncWithSystem() async  // Verify scheduled vs expected
    }
    
  • Add notification settings UI:
    • Enable/disable reminders
    • Set default reminder time
    • Set advance notice period
    • Sound selection
  • Handle notification permission denied gracefully
  • Register in DIContainer

Acceptance Criteria: Notifications fire at scheduled times with actionable buttons


End-of-Phase Validation

Functional Verification

Test Steps Expected Result Status
API Token Configured Build app No crash on Trefle token access [ ]
Plant Search Search "Monstera" Returns matching species [ ]
Species Detail Fetch species by slug Returns complete growth data [ ]
Care Info Display View identified plant Care requirements shown [ ]
Schedule Creation Add plant to collection Care schedule generated [ ]
Task List Open care schedule tab Upcoming tasks displayed [ ]
Task Completion Tap complete on task Task marked done, removed from list [ ]
Task Snooze Snooze task 1 hour Task rescheduled, notification updated [ ]
Notification Permission First launch Permission dialog shown [ ]
Notification Delivery Wait for scheduled time Notification appears [ ]
Notification Action Tap "Mark Complete" App opens, task completed [ ]
Offline Care Data Disable network Cached care info displayed [ ]
Unknown Species Search non-existent plant Graceful "not found" message [ ]
Calendar View Switch to calendar Tasks shown on correct dates [ ]
Filter Tasks Filter by "watering" Only watering tasks shown [ ]

Code Quality Verification

Check Criteria Status
Build Project builds with zero warnings [ ]
Architecture Trefle code isolated in Data/DataSources/Remote/TrefleAPI/ [ ]
Protocols All services use protocols for testability [ ]
Sendable All new types conform to Sendable [ ]
DTOs DTOs decode sample Trefle responses correctly [ ]
Mapper Mapper handles all optional fields with defaults [ ]
Use Cases Business logic in use cases, not ViewModels [ ]
DI Container New services registered in container [ ]
Error Types Trefle-specific errors defined [ ]
Unit Tests DTOs, mappers, and use cases have tests [ ]
Secrets API token not in source control [ ]
Notifications Permission handling follows Apple guidelines [ ]

Performance Verification

Metric Target Actual Status
Trefle Search Response < 2 seconds [ ]
Species Detail Fetch < 3 seconds [ ]
Care Schedule Generation < 100ms [ ]
Plant Detail View Load < 500ms [ ]
Care Schedule View Load < 300ms [ ]
Notification Scheduling (batch) < 1 second for 10 tasks [ ]
Care Info Cache Lookup < 50ms [ ]
Calendar View Render < 200ms [ ]

API Integration Verification

Test Steps Expected Result Status
Valid Species Search "Quercus robur" Returns oak species data [ ]
Growth Data Present Fetch species with growth Light, water, temp data present [ ]
Growth Data Missing Fetch species without growth Defaults used, no crash [ ]
Pagination Search common term Multiple pages available [ ]
Rate Limiting Make rapid requests 429 handled gracefully [ ]
Invalid Token Use wrong token Unauthorized error shown [ ]
Species Not Found Search gibberish Empty results, no error [ ]
Image URLs Fetch species Valid image URLs returned [ ]

Care Schedule Verification

Scenario Input Expected Output Status
Daily Watering High humidity plant Tasks every day [ ]
Weekly Watering Low humidity plant Tasks every 7 days [ ]
Monthly Fertilizer High nutrient need Tasks every 30 days [ ]
No Fertilizer Low nutrient need No fertilizer tasks [ ]
Seasonal Adjustment Outdoor plant in winter Reduced watering frequency [ ]
User Preferred Time Set 9:00 AM All tasks at 9:00 AM [ ]
30-Day Lookahead Create schedule Tasks for next 30 days [ ]
Task Completion Complete watering Next occurrence scheduled [ ]
Plant Deletion Delete plant All tasks removed [ ]

Notification Verification

Test Steps Expected Result Status
Permission Granted Accept notification prompt Reminders scheduled [ ]
Permission Denied Deny notification prompt Graceful fallback, in-app alerts [ ]
Notification Content Receive notification Correct plant name and task type [ ]
Complete Action Tap "Mark Complete" Task completed in app [ ]
Snooze Action Tap "Snooze" Notification rescheduled [ ]
Badge Count Have 3 overdue tasks Badge shows 3 [ ]
Badge Clear Complete all tasks Badge cleared [ ]
Background Delivery App closed Notification still fires [ ]
Notification Tap Tap notification Opens plant detail [ ]
Bulk Reschedule Complete task Future notifications updated [ ]

Phase 4 Completion Checklist

  • All 10 tasks completed (core implementation)
  • All functional tests pass
  • All code quality checks pass
  • All performance targets met
  • Trefle API integration verified
  • Care schedule generation working
  • Task management (complete/snooze/skip) working
  • Notifications scheduling and firing correctly
  • Notification actions handled properly
  • Offline mode works (cached care data)
  • API token secured (not in git)
  • Unit tests for DTOs, mappers, and use cases
  • UI tests for critical flows (view plant, complete task)
  • Code committed with descriptive message
  • Ready for Phase 5 (Plant Collection & Persistence)

Error Handling

Trefle API Errors

enum TrefleAPIError: Error, LocalizedError {
    case invalidToken
    case rateLimitExceeded
    case speciesNotFound(query: String)
    case serverError(statusCode: Int)
    case networkUnavailable
    case timeout
    case invalidResponse
    case paginationExhausted

    var errorDescription: String? {
        switch self {
        case .invalidToken:
            return "Invalid API token. Please check configuration."
        case .rateLimitExceeded:
            return "Too many requests. Please wait a moment."
        case .speciesNotFound(let query):
            return "No species found matching '\(query)'."
        case .serverError(let code):
            return "Server error (\(code)). Please try again later."
        case .networkUnavailable:
            return "No network connection."
        case .timeout:
            return "Request timed out. Please try again."
        case .invalidResponse:
            return "Invalid response from server."
        case .paginationExhausted:
            return "No more results available."
        }
    }
}

Care Schedule Errors

enum CareScheduleError: Error, LocalizedError {
    case noCareDataAvailable
    case schedulePersistenceFailed
    case invalidDateRange
    case plantNotFound

    var errorDescription: String? {
        switch self {
        case .noCareDataAvailable:
            return "Care information not available for this plant."
        case .schedulePersistenceFailed:
            return "Failed to save care schedule."
        case .invalidDateRange:
            return "Invalid date range for schedule."
        case .plantNotFound:
            return "Plant not found in collection."
        }
    }
}

Notification Errors

enum NotificationError: Error, LocalizedError {
    case permissionDenied
    case schedulingFailed
    case invalidTriggerDate
    case categoryNotRegistered

    var errorDescription: String? {
        switch self {
        case .permissionDenied:
            return "Notification permission denied. Enable in Settings."
        case .schedulingFailed:
            return "Failed to schedule reminder."
        case .invalidTriggerDate:
            return "Cannot schedule reminder for past date."
        case .categoryNotRegistered:
            return "Notification category not configured."
        }
    }
}

Notes

  • Trefle API has growth data for ~10% of species; implement graceful fallbacks
  • Cache Trefle responses aggressively (data rarely changes)
  • Notification limit: iOS allows ~64 pending local notifications
  • Schedule notifications in batches to stay under limit
  • Use background app refresh to reschedule notifications periodically
  • Consider user's timezone for notification scheduling
  • Trefle measurement units vary; normalize to metric internally, display in user's preference
  • Some plants need seasonal care adjustments (reduce watering in winter)
  • Badge count should only reflect overdue tasks, not all pending
  • Test notification actions with app in foreground, background, and terminated states

Dependencies

Dependency Type Notes
Trefle API External API 120 req/min rate limit
UserNotifications System Local notifications
URLSession System API requests
Core Data System Schedule persistence

Risk Mitigation

Risk Mitigation
Trefle API token exposed Use xcconfig, add to .gitignore
Species not in Trefle Provide generic care defaults
Missing growth data Use conservative defaults for watering/light
Notification permission denied In-app task list always available
Too many notifications Limit to 64, prioritize soonest tasks
User ignores reminders Badge count, overdue section in UI
Trefle API downtime Cache responses, retry with backoff
Incorrect care recommendations Add disclaimer, allow user overrides
Timezone issues Store all dates in UTC, convert for display
App deleted with pending notifications Notifications orphaned (OS handles cleanup)

Sample Trefle API Response

Search Response

{
  "data": [
    {
      "id": 834,
      "common_name": "Swiss cheese plant",
      "slug": "monstera-deliciosa",
      "scientific_name": "Monstera deliciosa",
      "year": 1849,
      "bibliography": "Vidensk. Meddel. Naturhist. Foren. Kjøbenhavn 1849: 19 (1849)",
      "author": "Liebm.",
      "family_common_name": "Arum family",
      "genus_id": 1254,
      "image_url": "https://bs.plantnet.org/image/o/abc123",
      "genus": "Monstera",
      "family": "Araceae"
    }
  ],
  "links": {
    "self": "/api/v1/plants/search?q=monstera",
    "first": "/api/v1/plants/search?page=1&q=monstera",
    "last": "/api/v1/plants/search?page=1&q=monstera"
  },
  "meta": {
    "total": 12
  }
}

Species Detail Response

{
  "data": {
    "id": 834,
    "common_name": "Swiss cheese plant",
    "slug": "monstera-deliciosa",
    "scientific_name": "Monstera deliciosa",
    "growth": {
      "light": 6,
      "atmospheric_humidity": 8,
      "minimum_temperature": {
        "deg_c": 15
      },
      "maximum_temperature": {
        "deg_c": 30
      },
      "soil_humidity": 7,
      "soil_nutriments": 5
    },
    "specifications": {
      "growth_rate": "moderate",
      "toxicity": "mild"
    }
  },
  "meta": {
    "last_modified": "2023-01-15T12:00:00Z"
  }
}

UI Mockups (Conceptual)

Plant Detail - Care Section

┌─────────────────────────────────────┐
│  ☀️ Light: Partial Sun (5-6 hrs)    │
│  💧 Water: Twice Weekly (Moderate)  │
│  🌡️ Temp: 15-30°C (Optimal: 22°C)  │
│  🌱 Fertilizer: Monthly (Balanced)  │
│  💨 Humidity: High                  │
└─────────────────────────────────────┘

Care Schedule - Task List

┌─────────────────────────────────────┐
│ OVERDUE (2)                         │
│ ┌─────────────────────────────────┐ │
│ │ 🪴 Monstera    💧 Water   [✓]  │ │
│ │ 🪴 Pothos      💧 Water   [✓]  │ │
│ └─────────────────────────────────┘ │
│                                     │
│ TODAY                               │
│ ┌─────────────────────────────────┐ │
│ │ 🪴 Ficus       🌱 Fertilize [✓]│ │
│ └─────────────────────────────────┘ │
│                                     │
│ TOMORROW                            │
│ ┌─────────────────────────────────┐ │
│ │ 🪴 Snake Plant 💧 Water   [○]  │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘