- 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>
362 lines
14 KiB
Swift
362 lines
14 KiB
Swift
//
|
|
// TrefleMapper.swift
|
|
// PlantGuide
|
|
//
|
|
// Created on 2026-01-21.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - TrefleMapper
|
|
|
|
/// Maps Trefle API DTOs to domain entities.
|
|
///
|
|
/// This mapper provides conversion functions for transforming Trefle API
|
|
/// responses into PlantCareInfo domain entities and related care requirement types.
|
|
/// All mapping functions handle nil/missing data gracefully with sensible defaults.
|
|
struct TrefleMapper {
|
|
|
|
// MARK: - Primary Mapping
|
|
|
|
/// Maps a Trefle species DTO to a PlantCareInfo domain entity.
|
|
///
|
|
/// This function extracts all available care information from the Trefle species data,
|
|
/// including growth requirements, environmental preferences, and blooming seasons.
|
|
///
|
|
/// - Parameter species: The detailed species information from the Trefle API.
|
|
/// - Returns: A `PlantCareInfo` domain entity populated with care requirements.
|
|
static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo {
|
|
let growth = species.growth
|
|
let specifications = species.specifications
|
|
|
|
return PlantCareInfo(
|
|
id: UUID(),
|
|
scientificName: species.scientificName,
|
|
commonName: species.commonName,
|
|
lightRequirement: mapToLightRequirement(from: growth?.light),
|
|
wateringSchedule: mapToWateringSchedule(from: growth),
|
|
temperatureRange: mapToTemperatureRange(from: growth),
|
|
fertilizerSchedule: mapToFertilizerSchedule(from: growth),
|
|
humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity),
|
|
growthRate: mapToGrowthRate(from: specifications?.growthRate),
|
|
bloomingSeason: mapToBloomingSeason(from: growth?.bloomMonths),
|
|
additionalNotes: buildAdditionalNotes(from: species),
|
|
sourceURL: URL(string: "https://trefle.io/api/v1/species/\(species.id)"),
|
|
trefleID: species.id
|
|
)
|
|
}
|
|
|
|
// MARK: - Light Requirement Mapping
|
|
|
|
/// Maps a Trefle light scale value to a LightRequirement enum.
|
|
///
|
|
/// Trefle uses a 0-10 scale where 0 is full shade and 10 is full sun.
|
|
/// This function maps that scale to the app's LightRequirement categories.
|
|
///
|
|
/// - Parameter light: The Trefle light value (0-10 scale), or nil if not available.
|
|
/// - Returns: The corresponding `LightRequirement` enum value.
|
|
/// Defaults to `.partialShade` if the input is nil.
|
|
///
|
|
/// Mapping:
|
|
/// - 0-2: `.fullShade` - Very low light conditions
|
|
/// - 3-4: `.lowLight` - Low light but not complete shade
|
|
/// - 5-6: `.partialShade` - Moderate, indirect light
|
|
/// - 7-10: `.fullSun` - Direct sunlight for most of the day
|
|
static func mapToLightRequirement(from light: Int?) -> LightRequirement {
|
|
guard let light = light else {
|
|
return .partialShade
|
|
}
|
|
|
|
switch light {
|
|
case 0...2:
|
|
return .fullShade
|
|
case 3...4:
|
|
return .lowLight
|
|
case 5...6:
|
|
return .partialShade
|
|
case 7...10:
|
|
return .fullSun
|
|
default:
|
|
return .partialShade
|
|
}
|
|
}
|
|
|
|
// MARK: - Watering Schedule Mapping
|
|
|
|
/// Maps Trefle growth data to a WateringSchedule.
|
|
///
|
|
/// This function determines watering frequency and amount based on the plant's
|
|
/// atmospheric and soil humidity requirements. Plants with higher humidity needs
|
|
/// typically require more frequent but lighter watering, while those with lower
|
|
/// needs benefit from less frequent but thorough watering.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing humidity requirements.
|
|
/// - Returns: A `WateringSchedule` with appropriate frequency and amount.
|
|
/// Defaults to weekly/moderate if growth data is nil.
|
|
///
|
|
/// Mapping based on humidity levels (0-10 scale):
|
|
/// - High humidity (7-10): Weekly frequency with light watering
|
|
/// - Medium humidity (4-6): Twice weekly with moderate watering
|
|
/// - Low humidity (0-3): Weekly with thorough watering
|
|
static func mapToWateringSchedule(from growth: TrefleGrowthDTO?) -> WateringSchedule {
|
|
guard let growth = growth else {
|
|
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
|
}
|
|
|
|
// Use atmospheric humidity as primary indicator, fall back to soil humidity
|
|
let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity
|
|
|
|
guard let humidity = humidityLevel else {
|
|
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
|
}
|
|
|
|
switch humidity {
|
|
case 7...10:
|
|
// High humidity plants need frequent, light watering
|
|
return WateringSchedule(frequency: .weekly, amount: .light)
|
|
case 4...6:
|
|
// Medium humidity plants need regular, moderate watering
|
|
return WateringSchedule(frequency: .twiceWeekly, amount: .moderate)
|
|
case 0...3:
|
|
// Low humidity plants (often drought-tolerant) need less frequent but thorough watering
|
|
return WateringSchedule(frequency: .weekly, amount: .thorough)
|
|
default:
|
|
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
|
}
|
|
}
|
|
|
|
// MARK: - Temperature Range Mapping
|
|
|
|
/// Maps Trefle growth data to a TemperatureRange.
|
|
///
|
|
/// This function extracts minimum and maximum temperature tolerances from the
|
|
/// Trefle growth data and determines frost tolerance based on whether the plant
|
|
/// can survive temperatures below 0 degrees Celsius.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing temperature information.
|
|
/// - Returns: A `TemperatureRange` with min/max values and frost tolerance.
|
|
/// Defaults to 15-30 degrees Celsius if growth data is nil.
|
|
static func mapToTemperatureRange(from growth: TrefleGrowthDTO?) -> TemperatureRange {
|
|
guard let growth = growth else {
|
|
return TemperatureRange(
|
|
minimumCelsius: 15.0,
|
|
maximumCelsius: 30.0,
|
|
optimalCelsius: nil,
|
|
frostTolerant: false
|
|
)
|
|
}
|
|
|
|
let minTemp = growth.minimumTemperature?.degC ?? 15.0
|
|
let maxTemp = growth.maximumTemperature?.degC ?? 30.0
|
|
|
|
// Calculate optimal as midpoint between min and max if both are available
|
|
let optimalTemp: Double?
|
|
if growth.minimumTemperature?.degC != nil && growth.maximumTemperature?.degC != nil {
|
|
optimalTemp = (minTemp + maxTemp) / 2.0
|
|
} else {
|
|
optimalTemp = nil
|
|
}
|
|
|
|
// Plant is frost tolerant if it can survive temperatures below 0 degrees Celsius
|
|
let frostTolerant = minTemp < 0.0
|
|
|
|
return TemperatureRange(
|
|
minimumCelsius: minTemp,
|
|
maximumCelsius: maxTemp,
|
|
optimalCelsius: optimalTemp,
|
|
frostTolerant: frostTolerant
|
|
)
|
|
}
|
|
|
|
// MARK: - Fertilizer Schedule Mapping
|
|
|
|
/// Maps Trefle growth data to a FertilizerSchedule.
|
|
///
|
|
/// This function determines fertilizer frequency and type based on the plant's
|
|
/// soil nutrient requirements. Plants with higher nutrient needs require more
|
|
/// frequent fertilization, while those with lower needs can use organic fertilizers
|
|
/// applied less frequently.
|
|
///
|
|
/// - Parameter growth: The Trefle growth data containing soil nutrient requirements.
|
|
/// - Returns: A `FertilizerSchedule` with appropriate frequency and type,
|
|
/// or nil if soil nutrient data is not available.
|
|
///
|
|
/// Mapping based on soil nutriment levels (0-10 scale):
|
|
/// - High needs (7-10): Biweekly with balanced fertilizer
|
|
/// - Medium needs (4-6): Monthly with balanced fertilizer
|
|
/// - Low needs (0-3): Quarterly with organic fertilizer
|
|
static func mapToFertilizerSchedule(from growth: TrefleGrowthDTO?) -> FertilizerSchedule? {
|
|
guard let soilNutriments = growth?.soilNutriments else {
|
|
return nil
|
|
}
|
|
|
|
switch soilNutriments {
|
|
case 7...10:
|
|
// High nutrient needs - frequent balanced fertilization
|
|
return FertilizerSchedule(frequency: .biweekly, type: .balanced)
|
|
case 4...6:
|
|
// Medium nutrient needs - monthly balanced fertilization
|
|
return FertilizerSchedule(frequency: .monthly, type: .balanced)
|
|
case 0...3:
|
|
// Low nutrient needs - occasional organic fertilization
|
|
return FertilizerSchedule(frequency: .quarterly, type: .organic)
|
|
default:
|
|
return FertilizerSchedule(frequency: .monthly, type: .balanced)
|
|
}
|
|
}
|
|
|
|
// MARK: - Humidity Level Mapping
|
|
|
|
/// Maps a Trefle atmospheric humidity value to a HumidityLevel enum.
|
|
///
|
|
/// Trefle uses a 0-10 scale for atmospheric humidity requirements.
|
|
/// This function maps that scale to the app's HumidityLevel categories.
|
|
///
|
|
/// - Parameter humidity: The Trefle atmospheric humidity value (0-10 scale),
|
|
/// or nil if not available.
|
|
/// - Returns: The corresponding `HumidityLevel` enum value, or nil if input is nil.
|
|
///
|
|
/// Mapping:
|
|
/// - 0-2: `.low` - Below 30% humidity
|
|
/// - 3-5: `.moderate` - 30-50% humidity
|
|
/// - 6-8: `.high` - 50-70% humidity
|
|
/// - 9-10: `.veryHigh` - Above 70% humidity
|
|
static func mapToHumidityLevel(from humidity: Int?) -> HumidityLevel? {
|
|
guard let humidity = humidity else {
|
|
return nil
|
|
}
|
|
|
|
switch humidity {
|
|
case 0...2:
|
|
return .low
|
|
case 3...5:
|
|
return .moderate
|
|
case 6...8:
|
|
return .high
|
|
case 9...10:
|
|
return .veryHigh
|
|
default:
|
|
return .moderate
|
|
}
|
|
}
|
|
|
|
// MARK: - Growth Rate Mapping
|
|
|
|
/// Maps a Trefle growth rate string to a GrowthRate enum.
|
|
///
|
|
/// Trefle provides growth rate as a string (e.g., "slow", "moderate", "rapid").
|
|
/// This function maps those values to the app's GrowthRate enum.
|
|
///
|
|
/// - Parameter growthRate: The Trefle growth rate string, or nil if not available.
|
|
/// - Returns: The corresponding `GrowthRate` enum value, or nil if input is nil
|
|
/// or doesn't match a known value.
|
|
static func mapToGrowthRate(from growthRate: String?) -> GrowthRate? {
|
|
guard let growthRate = growthRate?.lowercased() else {
|
|
return nil
|
|
}
|
|
|
|
switch growthRate {
|
|
case "slow":
|
|
return .slow
|
|
case "moderate", "medium":
|
|
return .moderate
|
|
case "rapid", "fast":
|
|
return .fast
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Blooming Season Mapping
|
|
|
|
/// Maps Trefle bloom months to an array of Season values.
|
|
///
|
|
/// Trefle provides bloom months as an array of three-letter month abbreviations
|
|
/// (e.g., ["mar", "apr", "may"]). This function converts those months to the
|
|
/// corresponding seasons based on Northern Hemisphere conventions.
|
|
///
|
|
/// - Parameter bloomMonths: An array of month abbreviations, or nil if not available.
|
|
/// - Returns: An array of unique `Season` values when the plant blooms,
|
|
/// or nil if bloom month data is not available.
|
|
///
|
|
/// Month to Season Mapping (Northern Hemisphere):
|
|
/// - December, January, February: `.winter`
|
|
/// - March, April, May: `.spring`
|
|
/// - June, July, August: `.summer`
|
|
/// - September, October, November: `.fall`
|
|
static func mapToBloomingSeason(from bloomMonths: [String]?) -> [Season]? {
|
|
guard let bloomMonths = bloomMonths, !bloomMonths.isEmpty else {
|
|
return nil
|
|
}
|
|
|
|
var seasons = Set<Season>()
|
|
|
|
for month in bloomMonths {
|
|
if let season = monthToSeason(month.lowercased()) {
|
|
seasons.insert(season)
|
|
}
|
|
}
|
|
|
|
guard !seasons.isEmpty else {
|
|
return nil
|
|
}
|
|
|
|
// Return seasons in natural order: spring, summer, fall, winter
|
|
let orderedSeasons: [Season] = [.spring, .summer, .fall, .winter]
|
|
return orderedSeasons.filter { seasons.contains($0) }
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
/// Converts a month abbreviation to its corresponding season.
|
|
///
|
|
/// - Parameter month: A three-letter month abbreviation (lowercase).
|
|
/// - Returns: The corresponding `Season`, or nil if the abbreviation is not recognized.
|
|
private static func monthToSeason(_ month: String) -> Season? {
|
|
switch month {
|
|
case "dec", "jan", "feb":
|
|
return .winter
|
|
case "mar", "apr", "may":
|
|
return .spring
|
|
case "jun", "jul", "aug":
|
|
return .summer
|
|
case "sep", "oct", "nov":
|
|
return .fall
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Builds additional care notes from species data.
|
|
///
|
|
/// This function compiles relevant information that doesn't fit into other
|
|
/// structured fields, such as pH requirements and toxicity warnings.
|
|
///
|
|
/// - Parameter species: The Trefle species DTO.
|
|
/// - Returns: A string containing additional care notes, or nil if no relevant data.
|
|
private static func buildAdditionalNotes(from species: TrefleSpeciesDTO) -> String? {
|
|
var notes: [String] = []
|
|
|
|
// Add pH range information if available
|
|
if let phMin = species.growth?.phMinimum, let phMax = species.growth?.phMaximum {
|
|
notes.append("Soil pH: \(String(format: "%.1f", phMin)) - \(String(format: "%.1f", phMax))")
|
|
} else if let phMin = species.growth?.phMinimum {
|
|
notes.append("Minimum soil pH: \(String(format: "%.1f", phMin))")
|
|
} else if let phMax = species.growth?.phMaximum {
|
|
notes.append("Maximum soil pH: \(String(format: "%.1f", phMax))")
|
|
}
|
|
|
|
// Add toxicity warning if available
|
|
if let toxicity = species.specifications?.toxicity?.lowercased(), toxicity != "none" {
|
|
notes.append("Toxicity: \(toxicity.capitalized)")
|
|
}
|
|
|
|
// Add family information for reference
|
|
if let family = species.family {
|
|
notes.append("Family: \(family)")
|
|
}
|
|
|
|
return notes.isEmpty ? nil : notes.joined(separator: ". ")
|
|
}
|
|
}
|