Fix plant care caching and add PlantNet API debug logging
- Fix Core Data transformer conflict causing cache decode failures - Changed PlantCareInfoMO attributes from Transformable to Binary type - Removed valueTransformerName references that conflicted with manual JSON encoding - Fix cache key mismatch causing cache misses - Normalize scientificName to lookup name when saving to cache - Remove cache expiration (cache now persistent until plant deleted) - Removed cacheExpiration property and isCacheStale check - Added forceRefresh parameter to FetchPlantCareUseCase for manual refresh - Add cache cleanup when deleting plants - Added PlantCareInfoRepositoryProtocol dependency to DeletePlantUseCase - Clean up cached care info when plant is deleted - Add extensive debug logging to PlantNetAPIService - Log request/response details, status codes, and decoding errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -410,7 +410,8 @@ final class DIContainer: DIContainerProtocol, ObservableObject {
|
||||
plantRepository: plantCollectionRepository,
|
||||
imageStorage: imageStorage,
|
||||
notificationService: notificationService,
|
||||
careScheduleRepository: careScheduleRepository
|
||||
careScheduleRepository: careScheduleRepository,
|
||||
plantCareInfoRepository: plantCareInfoRepository
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
</entity>
|
||||
<entity name="PlantCareInfoMO" representedClassName="PlantCareInfoMO" syncable="YES">
|
||||
<attribute name="additionalNotes" optional="YES" attributeType="String"/>
|
||||
<attribute name="bloomingSeasonData" optional="YES" attributeType="Transformable" valueTransformerName="SeasonArrayTransformer"/>
|
||||
<attribute name="bloomingSeasonData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="commonName" optional="YES" attributeType="String"/>
|
||||
<attribute name="fertilizerScheduleData" optional="YES" attributeType="Transformable" valueTransformerName="FertilizerScheduleTransformer"/>
|
||||
<attribute name="fertilizerScheduleData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="fetchedAt" attributeType="Date" usesScalarType="NO"/>
|
||||
<attribute name="growthRate" optional="YES" attributeType="String"/>
|
||||
<attribute name="humidity" optional="YES" attributeType="String"/>
|
||||
@@ -82,9 +82,9 @@
|
||||
<attribute name="lightRequirement" attributeType="String"/>
|
||||
<attribute name="scientificName" attributeType="String"/>
|
||||
<attribute name="sourceURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="temperatureRangeData" attributeType="Transformable" valueTransformerName="TemperatureRangeTransformer"/>
|
||||
<attribute name="temperatureRangeData" attributeType="Binary"/>
|
||||
<attribute name="trefleID" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarType="YES"/>
|
||||
<attribute name="wateringScheduleData" attributeType="Transformable" valueTransformerName="WateringScheduleTransformer"/>
|
||||
<attribute name="wateringScheduleData" attributeType="Binary"/>
|
||||
<relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="plantCareInfo" inverseEntity="PlantMO"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
|
||||
@@ -234,9 +234,16 @@ final class PlantNetAPIService: PlantNetAPIServiceProtocol, @unchecked Sendable
|
||||
organs: [PlantOrgan],
|
||||
project: PlantNetProject
|
||||
) async throws -> PlantNetIdentifyResponseDTO {
|
||||
print("[PlantNet DEBUG] ========== Starting Identification ==========")
|
||||
print("[PlantNet DEBUG] Image data size: \(imageData.count) bytes")
|
||||
print("[PlantNet DEBUG] Organs: \(organs.map { $0.rawValue })")
|
||||
print("[PlantNet DEBUG] Project: \(project.rawValue)")
|
||||
print("[PlantNet DEBUG] API Key present: \(!apiKey.isEmpty), length: \(apiKey.count)")
|
||||
|
||||
// Check rate limit before making request
|
||||
if let tracker = rateLimitTracker {
|
||||
guard await tracker.canMakeRequest() else {
|
||||
print("[PlantNet DEBUG] ERROR: Rate limit exhausted")
|
||||
logError("Rate limit exhausted")
|
||||
throw PlantNetAPIError.rateLimitExceeded
|
||||
}
|
||||
@@ -244,10 +251,15 @@ final class PlantNetAPIService: PlantNetAPIServiceProtocol, @unchecked Sendable
|
||||
|
||||
// Build URL with query parameters
|
||||
guard let url = buildIdentifyURL(project: project) else {
|
||||
print("[PlantNet DEBUG] ERROR: Failed to build identify URL")
|
||||
logError("Failed to build identify URL")
|
||||
throw PlantNetAPIError.invalidResponse
|
||||
}
|
||||
|
||||
// Log URL without API key for security
|
||||
let sanitizedURL = url.absoluteString.replacingOccurrences(of: apiKey, with: "***API_KEY***")
|
||||
print("[PlantNet DEBUG] Request URL: \(sanitizedURL)")
|
||||
|
||||
// Create multipart request
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
@@ -261,6 +273,9 @@ final class PlantNetAPIService: PlantNetAPIServiceProtocol, @unchecked Sendable
|
||||
boundary: boundary
|
||||
)
|
||||
|
||||
print("[PlantNet DEBUG] Request body size: \(request.httpBody?.count ?? 0) bytes")
|
||||
print("[PlantNet DEBUG] Content-Type: \(request.value(forHTTPHeaderField: "Content-Type") ?? "nil")")
|
||||
|
||||
logRequest(request)
|
||||
|
||||
// Perform request
|
||||
@@ -268,34 +283,70 @@ final class PlantNetAPIService: PlantNetAPIServiceProtocol, @unchecked Sendable
|
||||
let response: URLResponse
|
||||
|
||||
do {
|
||||
print("[PlantNet DEBUG] Sending request...")
|
||||
(data, response) = try await session.data(for: request)
|
||||
print("[PlantNet DEBUG] Request completed, received \(data.count) bytes")
|
||||
} catch let error as URLError {
|
||||
print("[PlantNet DEBUG] URLError: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
logError("Request failed: \(error.localizedDescription)")
|
||||
if error.code == .notConnectedToInternet || error.code == .networkConnectionLost {
|
||||
throw PlantNetAPIError.networkUnavailable
|
||||
}
|
||||
throw PlantNetAPIError.imageUploadFailed
|
||||
} catch {
|
||||
print("[PlantNet DEBUG] Request error: \(error)")
|
||||
logError("Request failed: \(error.localizedDescription)")
|
||||
throw PlantNetAPIError.imageUploadFailed
|
||||
}
|
||||
|
||||
// Log response details
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
print("[PlantNet DEBUG] Response status code: \(httpResponse.statusCode)")
|
||||
print("[PlantNet DEBUG] Response headers: \(httpResponse.allHeaderFields)")
|
||||
}
|
||||
|
||||
// Always log response body for debugging
|
||||
if let responseBody = String(data: data, encoding: .utf8) {
|
||||
print("[PlantNet DEBUG] Response body: \(responseBody)")
|
||||
} else {
|
||||
print("[PlantNet DEBUG] Response body: (non-UTF8 data, \(data.count) bytes)")
|
||||
}
|
||||
|
||||
// Validate response
|
||||
try await validateResponse(response, data: data)
|
||||
logResponse(response, data: data)
|
||||
|
||||
// Decode response
|
||||
do {
|
||||
print("[PlantNet DEBUG] Decoding response...")
|
||||
let identifyResponse = try decoder.decode(PlantNetIdentifyResponseDTO.self, from: data)
|
||||
print("[PlantNet DEBUG] Decoded successfully! Results count: \(identifyResponse.results.count)")
|
||||
|
||||
// Update rate limit tracker with remaining count
|
||||
if let tracker = rateLimitTracker,
|
||||
let remaining = identifyResponse.remainingIdentificationRequests {
|
||||
print("[PlantNet DEBUG] Remaining requests: \(remaining)")
|
||||
await tracker.recordUsage(remaining: remaining)
|
||||
}
|
||||
|
||||
print("[PlantNet DEBUG] ========== Identification Complete ==========")
|
||||
return identifyResponse
|
||||
} catch {
|
||||
print("[PlantNet DEBUG] Decoding failed: \(error)")
|
||||
if let decodingError = error as? DecodingError {
|
||||
switch decodingError {
|
||||
case .keyNotFound(let key, let context):
|
||||
print("[PlantNet DEBUG] Key not found: \(key.stringValue), path: \(context.codingPath)")
|
||||
case .typeMismatch(let type, let context):
|
||||
print("[PlantNet DEBUG] Type mismatch: expected \(type), path: \(context.codingPath)")
|
||||
case .valueNotFound(let type, let context):
|
||||
print("[PlantNet DEBUG] Value not found: \(type), path: \(context.codingPath)")
|
||||
case .dataCorrupted(let context):
|
||||
print("[PlantNet DEBUG] Data corrupted: \(context.debugDescription)")
|
||||
@unknown default:
|
||||
print("[PlantNet DEBUG] Unknown decoding error")
|
||||
}
|
||||
}
|
||||
logError("Decoding failed: \(error)")
|
||||
throw PlantNetAPIError.invalidResponse
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@ enum DeletePlantError: Error, LocalizedError {
|
||||
/// 1. Validates that the plant exists
|
||||
/// 2. Cancels all scheduled notifications for the plant
|
||||
/// 3. Deletes cached images from local storage
|
||||
/// 4. Deletes the care schedule
|
||||
/// 5. Deletes the plant entity from the repository
|
||||
/// 4. Deletes cached plant care info
|
||||
/// 5. Deletes the care schedule
|
||||
/// 6. Deletes the plant entity from the repository
|
||||
///
|
||||
/// The deletion is performed in order to ensure proper cleanup even if
|
||||
/// some operations fail. The plant is deleted last to ensure all associated
|
||||
@@ -124,7 +125,8 @@ enum DeletePlantError: Error, LocalizedError {
|
||||
/// plantRepository: plantRepository,
|
||||
/// imageStorage: imageStorage,
|
||||
/// notificationService: notificationService,
|
||||
/// careScheduleRepository: careScheduleRepository
|
||||
/// careScheduleRepository: careScheduleRepository,
|
||||
/// plantCareInfoRepository: plantCareInfoRepository
|
||||
/// )
|
||||
///
|
||||
/// try await useCase.execute(plantID: plant.id)
|
||||
@@ -137,6 +139,7 @@ final class DeletePlantUseCase: DeletePlantUseCaseProtocol, @unchecked Sendable
|
||||
private let imageStorage: ImageStorageProtocol
|
||||
private let notificationService: NotificationServiceProtocol
|
||||
private let careScheduleRepository: CareScheduleRepositoryProtocol
|
||||
private let plantCareInfoRepository: PlantCareInfoRepositoryProtocol?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -147,16 +150,19 @@ final class DeletePlantUseCase: DeletePlantUseCaseProtocol, @unchecked Sendable
|
||||
/// - imageStorage: Service for deleting stored plant images.
|
||||
/// - notificationService: Service for cancelling scheduled notifications.
|
||||
/// - careScheduleRepository: Repository for deleting care schedules.
|
||||
/// - plantCareInfoRepository: Optional repository for deleting cached care info.
|
||||
init(
|
||||
plantRepository: PlantCollectionRepositoryProtocol,
|
||||
imageStorage: ImageStorageProtocol,
|
||||
notificationService: NotificationServiceProtocol,
|
||||
careScheduleRepository: CareScheduleRepositoryProtocol
|
||||
careScheduleRepository: CareScheduleRepositoryProtocol,
|
||||
plantCareInfoRepository: PlantCareInfoRepositoryProtocol? = nil
|
||||
) {
|
||||
self.plantRepository = plantRepository
|
||||
self.imageStorage = imageStorage
|
||||
self.notificationService = notificationService
|
||||
self.careScheduleRepository = careScheduleRepository
|
||||
self.plantCareInfoRepository = plantCareInfoRepository
|
||||
}
|
||||
|
||||
// MARK: - DeletePlantUseCaseProtocol
|
||||
@@ -175,11 +181,15 @@ final class DeletePlantUseCase: DeletePlantUseCaseProtocol, @unchecked Sendable
|
||||
// Log errors but continue with deletion
|
||||
await deleteImages(for: plantID)
|
||||
|
||||
// Step 4: Delete care schedule
|
||||
// Step 4: Delete cached plant care info
|
||||
// Log errors but continue with deletion
|
||||
await deleteCachedCareInfo(for: plantID)
|
||||
|
||||
// Step 5: Delete care schedule
|
||||
// This should cascade from the repository but we explicitly delete for safety
|
||||
await deleteCareSchedule(for: plantID)
|
||||
|
||||
// Step 5: Delete the plant from repository
|
||||
// Step 6: Delete the plant from repository
|
||||
do {
|
||||
try await plantRepository.delete(id: plantID)
|
||||
} catch {
|
||||
@@ -226,4 +236,20 @@ final class DeletePlantUseCase: DeletePlantUseCaseProtocol, @unchecked Sendable
|
||||
print("Warning: Failed to delete care schedule for plant \(plantID): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes cached plant care info for a plant.
|
||||
///
|
||||
/// Errors are logged but do not prevent plant deletion.
|
||||
///
|
||||
/// - Parameter plantID: The unique identifier of the plant.
|
||||
private func deleteCachedCareInfo(for plantID: UUID) async {
|
||||
guard let repository = plantCareInfoRepository else { return }
|
||||
|
||||
do {
|
||||
try await repository.delete(for: plantID)
|
||||
} catch {
|
||||
// Log the error but don't prevent deletion
|
||||
print("Warning: Failed to delete cached care info for plant \(plantID): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ protocol FetchPlantCareUseCaseProtocol: Sendable {
|
||||
/// the scientific name, retrieves detailed species information,
|
||||
/// and maps it to a `PlantCareInfo` domain entity.
|
||||
///
|
||||
/// - Parameter scientificName: The scientific (botanical) name of the plant
|
||||
/// (e.g., "Rosa gallica").
|
||||
/// - Parameters:
|
||||
/// - scientificName: The scientific (botanical) name of the plant (e.g., "Rosa gallica").
|
||||
/// - forceRefresh: If true, bypasses the cache and fetches fresh data from the API.
|
||||
/// - Returns: A `PlantCareInfo` entity containing care requirements.
|
||||
/// - Throws: `FetchPlantCareError` if the plant cannot be found or data retrieval fails.
|
||||
///
|
||||
@@ -31,7 +32,7 @@ protocol FetchPlantCareUseCaseProtocol: Sendable {
|
||||
/// let careInfo = try await useCase.execute(scientificName: "Rosa gallica")
|
||||
/// print("Light: \(careInfo.lightRequirement)")
|
||||
/// ```
|
||||
func execute(scientificName: String) async throws -> PlantCareInfo
|
||||
func execute(scientificName: String, forceRefresh: Bool) async throws -> PlantCareInfo
|
||||
|
||||
/// Fetches care information for a plant by its Trefle species ID.
|
||||
///
|
||||
@@ -39,7 +40,9 @@ protocol FetchPlantCareUseCaseProtocol: Sendable {
|
||||
/// the numeric Trefle ID and maps it to a `PlantCareInfo` domain entity.
|
||||
/// This is more efficient than searching by name when the ID is already known.
|
||||
///
|
||||
/// - Parameter trefleId: The numeric identifier for the species in the Trefle database.
|
||||
/// - Parameters:
|
||||
/// - trefleId: The numeric identifier for the species in the Trefle database.
|
||||
/// - forceRefresh: If true, bypasses the cache and fetches fresh data from the API.
|
||||
/// - Returns: A `PlantCareInfo` entity containing care requirements.
|
||||
/// - Throws: `FetchPlantCareError` if the species cannot be found or data retrieval fails.
|
||||
///
|
||||
@@ -48,7 +51,7 @@ protocol FetchPlantCareUseCaseProtocol: Sendable {
|
||||
/// let careInfo = try await useCase.execute(trefleId: 123456)
|
||||
/// print("Watering: \(careInfo.wateringSchedule.frequency)")
|
||||
/// ```
|
||||
func execute(trefleId: Int) async throws -> PlantCareInfo
|
||||
func execute(trefleId: Int, forceRefresh: Bool) async throws -> PlantCareInfo
|
||||
}
|
||||
|
||||
// MARK: - FetchPlantCareError
|
||||
@@ -118,13 +121,15 @@ enum FetchPlantCareError: Error, LocalizedError {
|
||||
/// Use case for fetching plant care information from the Trefle botanical API.
|
||||
///
|
||||
/// This use case coordinates the retrieval of plant care data by:
|
||||
/// 1. Checking local cache first for previously fetched care info
|
||||
/// 2. Validating cache freshness (7-day expiration by default)
|
||||
/// 3. Searching for plants by scientific name or fetching directly by ID
|
||||
/// 4. Retrieving detailed species information including growth requirements
|
||||
/// 5. Mapping API responses to domain entities using `TrefleMapper`
|
||||
/// 6. Caching API responses for future use
|
||||
/// 7. Providing fallback default care information when API data is incomplete
|
||||
/// 1. Checking local cache first for previously fetched care info (persistent, no expiration)
|
||||
/// 2. Searching for plants by scientific name or fetching directly by ID
|
||||
/// 3. Retrieving detailed species information including growth requirements
|
||||
/// 4. Mapping API responses to domain entities using `TrefleMapper`
|
||||
/// 5. Caching API responses for future use
|
||||
///
|
||||
/// Cache is persistent until:
|
||||
/// - The user manually refreshes via forceRefresh parameter
|
||||
/// - The plant is deleted from the collection
|
||||
///
|
||||
/// Usage:
|
||||
/// ```swift
|
||||
@@ -136,8 +141,8 @@ enum FetchPlantCareError: Error, LocalizedError {
|
||||
/// // Fetch by scientific name (checks cache first)
|
||||
/// let careInfo = try await useCase.execute(scientificName: "Rosa gallica")
|
||||
///
|
||||
/// // Or fetch by Trefle ID
|
||||
/// let careInfo = try await useCase.execute(trefleId: 123456)
|
||||
/// // Force refresh from API (bypasses cache)
|
||||
/// let freshCareInfo = try await useCase.execute(scientificName: "Rosa gallica", forceRefresh: true)
|
||||
/// ```
|
||||
final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sendable {
|
||||
|
||||
@@ -146,11 +151,6 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
||||
private let trefleAPIService: TrefleAPIServiceProtocol
|
||||
private let cacheRepository: PlantCareInfoRepositoryProtocol?
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// Cache expiration duration (7 days in seconds)
|
||||
private let cacheExpiration: TimeInterval = 7 * 24 * 60 * 60
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Creates a new FetchPlantCareUseCase instance.
|
||||
@@ -168,42 +168,54 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
||||
|
||||
// MARK: - FetchPlantCareUseCaseProtocol
|
||||
|
||||
func execute(scientificName: String) async throws -> PlantCareInfo {
|
||||
// 1. Check cache first
|
||||
if let cached = try? await fetchFromCache(scientificName: scientificName) {
|
||||
func execute(scientificName: String, forceRefresh: Bool = false) async throws -> PlantCareInfo {
|
||||
// 1. Check cache first (unless force refresh is requested)
|
||||
if !forceRefresh, let cached = try? await fetchFromCache(scientificName: scientificName) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// 2. Fetch from API
|
||||
let careInfo = try await fetchFromAPI(scientificName: scientificName)
|
||||
|
||||
// 3. Cache the result (fire and forget, don't block on cache errors)
|
||||
// 3. Cache the result with the lookup name as the key
|
||||
// The API may return a different scientific name (e.g., with author suffix),
|
||||
// so we normalize to the lookup name to ensure cache hits on subsequent lookups.
|
||||
let careInfoForCache = PlantCareInfo(
|
||||
id: careInfo.id,
|
||||
scientificName: scientificName, // Use lookup name as cache key
|
||||
commonName: careInfo.commonName,
|
||||
lightRequirement: careInfo.lightRequirement,
|
||||
wateringSchedule: careInfo.wateringSchedule,
|
||||
temperatureRange: careInfo.temperatureRange,
|
||||
fertilizerSchedule: careInfo.fertilizerSchedule,
|
||||
humidity: careInfo.humidity,
|
||||
growthRate: careInfo.growthRate,
|
||||
bloomingSeason: careInfo.bloomingSeason,
|
||||
additionalNotes: careInfo.additionalNotes,
|
||||
sourceURL: careInfo.sourceURL,
|
||||
trefleID: careInfo.trefleID
|
||||
)
|
||||
|
||||
// Cache the result (fire and forget, don't block on cache errors)
|
||||
Task {
|
||||
try? await cacheRepository?.save(careInfo, for: nil)
|
||||
try? await cacheRepository?.save(careInfoForCache, for: nil)
|
||||
}
|
||||
|
||||
return careInfo
|
||||
}
|
||||
|
||||
/// Fetches care info from cache if it exists and is not stale.
|
||||
/// Fetches care info from cache if it exists.
|
||||
///
|
||||
/// Cache is persistent (no expiration) - it persists until the plant is deleted
|
||||
/// or the user manually refreshes.
|
||||
///
|
||||
/// - Parameter scientificName: The scientific name of the plant.
|
||||
/// - Returns: Cached PlantCareInfo if valid, nil otherwise.
|
||||
/// - Returns: Cached PlantCareInfo if available, nil otherwise.
|
||||
private func fetchFromCache(scientificName: String) async throws -> PlantCareInfo? {
|
||||
guard let repository = cacheRepository else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if cache is stale
|
||||
let isStale = try await repository.isCacheStale(
|
||||
scientificName: scientificName,
|
||||
cacheExpiration: cacheExpiration
|
||||
)
|
||||
|
||||
if isStale {
|
||||
return nil
|
||||
}
|
||||
|
||||
return try await repository.fetch(scientificName: scientificName)
|
||||
}
|
||||
|
||||
@@ -245,9 +257,9 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
||||
}
|
||||
}
|
||||
|
||||
func execute(trefleId: Int) async throws -> PlantCareInfo {
|
||||
// 1. Check cache first
|
||||
if let cached = try? await cacheRepository?.fetch(trefleID: trefleId) {
|
||||
func execute(trefleId: Int, forceRefresh: Bool = false) async throws -> PlantCareInfo {
|
||||
// 1. Check cache first (unless force refresh is requested)
|
||||
if !forceRefresh, let cached = try? await cacheRepository?.fetch(trefleID: trefleId) {
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
@@ -108,14 +108,19 @@ final class PlantDetailViewModel {
|
||||
///
|
||||
/// This method fetches care details from the Trefle botanical API
|
||||
/// and checks for existing care schedules.
|
||||
func loadCareInfo() async {
|
||||
///
|
||||
/// - Parameter forceRefresh: If true, bypasses the cache and fetches fresh data from the API.
|
||||
func loadCareInfo(forceRefresh: Bool = false) async {
|
||||
isLoading = true
|
||||
error = nil
|
||||
successMessage = nil
|
||||
|
||||
do {
|
||||
// Fetch care info from API
|
||||
let info = try await fetchPlantCareUseCase.execute(scientificName: plant.scientificName)
|
||||
// Fetch care info (from cache or API based on forceRefresh)
|
||||
let info = try await fetchPlantCareUseCase.execute(
|
||||
scientificName: plant.scientificName,
|
||||
forceRefresh: forceRefresh
|
||||
)
|
||||
careInfo = info
|
||||
|
||||
// Check for existing schedule
|
||||
@@ -289,9 +294,11 @@ final class PlantDetailViewModel {
|
||||
successMessage = nil
|
||||
}
|
||||
|
||||
/// Refreshes all plant data
|
||||
/// Refreshes all plant data by fetching fresh care info from the API
|
||||
///
|
||||
/// This bypasses the cache and fetches updated data from the Trefle API.
|
||||
func refresh() async {
|
||||
await loadCareInfo()
|
||||
await loadCareInfo(forceRefresh: true)
|
||||
}
|
||||
|
||||
/// Requests notification permission from the user
|
||||
|
||||
Reference in New Issue
Block a user