diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Data/DataModels.swift index 98105d4..33ba8c5 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataModels.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataModels.swift @@ -63,6 +63,7 @@ struct RecipeDetail: Codable { } struct RecipeImage { + var imageExists: Bool = true var thumb: UIImage? var full: UIImage? } diff --git a/Nextcloud Cookbook iOS Client/Data/DataStore.swift b/Nextcloud Cookbook iOS Client/Data/DataStore.swift index 65845c2..42b9380 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataStore.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataStore.swift @@ -16,10 +16,18 @@ class DataStore { appropriateFor: nil, create: false ) - .appendingPathComponent(appending) } + private static func fileURL() throws -> URL { + try FileManager.default.url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false + ) + } + func load(fromPath path: String) async throws -> D? { let task = Task { let fileURL = try Self.fileURL(appending: path) @@ -45,12 +53,24 @@ class DataStore { } } - func clearAll() { + func clearAll() -> Bool { + print("Attempting to delete all data ...") + let fm = FileManager.default + guard let folderPath = fm.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false } + print("Folder path: ", folderPath) do { - try FileManager.default.removeItem(at: Self.fileURL(appending: "")) + let filePaths = try fm.contentsOfDirectory(atPath: folderPath) + for filePath in filePaths { + print("File path: ", filePath) + try fm.removeItem(atPath: folderPath + filePath) + } } catch { - print("Could not delete file, probably read-only filesystem") + print("Could not delete documents folder contents: \(error)") + return false } + print("Done.") + return true + } } diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index ce579f3..c57cedc 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -77,22 +77,36 @@ import UIKit /// - needsUpdate: Determines wether the image should be loaded directly from the server, or if it should be loaded from cache/store first. /// - Returns: The image if found locally or on the server, otherwise nil. func loadImage(recipeId: Int, full: Bool, needsUpdate: Bool = false) async -> UIImage? { - print("loadImage(recipeId: \(recipeId), full: \(full), needsUpdate: \(needsUpdate)") + print("loadImage(recipeId: \(recipeId), full: \(full), needsUpdate: \(needsUpdate))") // If the image needs an update, request it from the server and overwrite the stored image if needsUpdate { if let data = await imageDataFromServer(recipeId: recipeId, full: full) { - guard let image = UIImage(data: data) else { return nil } + guard let image = UIImage(data: data) else { + imageCache[recipeId] = RecipeImage(imageExists: false) + return nil + } await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full)) imageToCache(image: image, recipeId: recipeId, full: full) return image + } else { + imageCache[recipeId] = RecipeImage(imageExists: false) + return nil } } + + // Check imageExists flag to detect if we attempted to load a non-existing image before. + // This allows us to avoid sending requests to the server if we already know the recipe has no image. + if imageCache[recipeId] != nil { + guard imageCache[recipeId]!.imageExists else { return nil } + } + // Try to load image from cache print("Attempting to load image from cache ...") - if imageCache[recipeId] != nil { + if let image = imageFromCache(recipeId: recipeId, full: full) { print("Image found in cache.") - return imageFromCache(recipeId: recipeId, full: full) + return image } + // Try to load from store print("Attempting to load image from local storage ...") if let image = await imageFromStore(recipeId: recipeId, full: full) { @@ -100,18 +114,32 @@ import UIKit imageToCache(image: image, recipeId: recipeId, full: full) return image } + // Try to load from the server. Store if successfull. print("Attempting to load image from server ...") if let data = await imageDataFromServer(recipeId: recipeId, full: full) { print("Image data received.") - imageCache[recipeId] = RecipeImage() // Create empty RecipeImage for each recipe even if no image found, so that further server requests are only sent if explicitly requested. - guard let image = UIImage(data: data) else { return nil } + // Create empty RecipeImage for each recipe even if no image found, so that further server requests are only sent if explicitly requested. + guard let image = UIImage(data: data) else { + imageCache[recipeId] = RecipeImage(imageExists: false) + return nil + } await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full)) imageToCache(image: image, recipeId: recipeId, full: full) return image } + imageCache[recipeId] = RecipeImage(imageExists: false) return nil } + + func deleteAllData() { + if dataStore.clearAll() { + self.categories = [] + self.recipes = [:] + self.imageCache = [:] + self.recipeDetails = [:] + } + } } @@ -140,11 +168,13 @@ extension MainViewModel { private func imageToCache(image: UIImage, recipeId: Int, full: Bool) { if imageCache[recipeId] == nil { - imageCache[recipeId] = RecipeImage() + imageCache[recipeId] = RecipeImage(imageExists: true) } if full { + imageCache[recipeId]!.imageExists = true imageCache[recipeId]!.full = image } else { + imageCache[recipeId]!.imageExists = true imageCache[recipeId]!.thumb = image } } diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index 9947931..36592c5 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -30,7 +30,7 @@ struct MainView: View { } .navigationTitle("CookBook") .toolbar { - NavigationLink( destination: SettingsView(userSettings: userSettings)) { + NavigationLink( destination: SettingsView(userSettings: userSettings, viewModel: viewModel)) { Image(systemName: "gear") } } diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index 31bb2c6..c65b695 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -10,6 +10,7 @@ import SwiftUI struct SettingsView: View { @ObservedObject var userSettings: UserSettings + @ObservedObject var viewModel: MainViewModel var body: some View { List { @@ -33,7 +34,7 @@ struct SettingsView: View { { Button("Clear Cache") { print("Clear cache.") - + viewModel.deleteAllData() } .buttonStyle(.borderedProminent) .accentColor(.red)