我下载(这里模拟)了一堆 webApp 的一些数据。我为每个文件下载一个图像,将它们存储在磁盘上,并将图像的路径以及单个 Web 应用程序的其余信息保存在 coreData 中。
我的问题是:第一次运行时,一切正常,开始时显示默认图像,然后更新它们。图像保存在磁盘上,一切正常。从第二次运行开始,图像似乎没有保存(我检查过)并且找不到它们。我认为我在包装/读取路径方面存在一些问题,否则在 ui 更新方式方面存在一些问题。
我通过 Xcode 检查了应用程序容器内部,图像存在。
import CoreData
import Foundation
class PersistenceController: ObservableObject {
var viewContext: NSManagedObjectContext
let container: NSPersistentContainer
//PreferredApps
init() {
container = NSPersistentContainer(name: "PersistentAppList")
viewContext = container.viewContext
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description?.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
container.loadPersistentStores { description, error in
if let error {
print("❌ error: \(error.localizedDescription)")
} else {
print("✅ loaded persistent stores")
}
}
}
}
//*****************************************************************
import CoreData
import SwiftUI
class ClassFromEntryPoint: ObservableObject {
@Published var downloadedAppsList: [WebAppLocalModel] = [] // Contains the entire downloaded app list
@Published var availableAppsList: [WebAppLocalModel] = [] // Shows apps present both in core data and in the downloaded app list
@Published var isLoading = false // Used for activityIndicator
var completePureValueFromCoreData: [WebApp] {
let fetchRequest: NSFetchRequest<WebApp> = WebApp.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor] // Apply the sortDescriptor
do {
return try persistenceController.container.viewContext.fetch(fetchRequest)
} catch {
print("Error fetching apps: \(error)")
}
return [] // In case of failure return empty array
}
let persistenceController: PersistenceController // Used for dependency injection
let context: NSManagedObjectContext
init(persistenceController: PersistenceController) {
print("ClassFromEntryPoint initializied")
self.persistenceController = persistenceController
self.context = persistenceController.container.viewContext
performMainCallAndUpdate()
}
func performMainCallAndUpdate() {
Task {
// Download and fill the downloaded list
await self.downloadListFromWeb()
// Verify if some of them must be added to the persistent store
ifDownloadedSomeNewAppSaveItOnPersistentStore() //cicles and donwloads and saves images
updateAvailableAppsListFilteredByDownloadedItems()
}
}
func updateDownloadedAppsListFromCoreData() {
let appsFromCoreData = self.completePureValueFromCoreData // Recupera le app da CoreData
var updatedDownloadedList: [WebAppLocalModel] = []
// Converti ogni WebApp (CoreData) in WebAppLocalModel (per la lista scaricata)
for app in appsFromCoreData {
let localModel = WebAppLocalModel(webAppFromCoreData: app)
updatedDownloadedList.append(localModel)
}
DispatchQueue.main.async {
self.downloadedAppsList = updatedDownloadedList // Aggiorna la lista scaricata
}
}
@MainActor
func downloadListFromWeb() async {
print("downloadListFromWeb called")
isLoading = true
defer { isLoading = false }
// Simulate a network call
do {
self.downloadedAppsList = try await simulateNetworkCall()
print("✅ filled downloadedAppsList")
} catch {
// Handle error
print("❌ Error during network call simulation: \(error)")
}
}
// Function to simulate an asynchronous network call
private func simulateNetworkCall() async throws -> [WebAppLocalModel] {
try await Task.sleep(nanoseconds: 2_000_000_000) // Wait 2 seconds
var listToReturn = [WebAppLocalModel]()
for item in 1...5 {
listToReturn.append(WebAppLocalModel(
id: "\(item)",
name: "name \(item)",
logoUrl: "https://hws.dev/img/logo.png")
)
}
return listToReturn
}
// MARK: saving on coredata after download
/// Used to save any NEW element received from the server. To simulate, comment out the relevant element where you download the list from the internet
func ifDownloadedSomeNewAppSaveItOnPersistentStore() {
defer {
print("verified if there are new apps")
//updateLists()
}
for downloaded in self.downloadedAppsList {
if self.completePureValueFromCoreData.contains(where: {$0.id == downloaded.id}) {
print("id already present, not saving name: \(downloaded.name) id: \(downloaded.id)")
} else {
self.saveOnCoreData(webAppLocalModel: downloaded)
}
}
}
func saveOnCoreData(webAppLocalModel: WebAppLocalModel) {
let newApp = WebApp(context: self.context)
newApp.id = webAppLocalModel.id
newApp.name = webAppLocalModel.name
newApp.logoUrl = webAppLocalModel.logoUrl
newApp.localLogoUrl = webAppLocalModel.localLogoUrl
let _ = print("Saving on CoreData...")
do {
try self.context.save()
let _ = print("... ✅ saved \(webAppLocalModel.name)")
} catch {
print("❌ \(error.localizedDescription)")
}
Task {
await downloadAndSaveImage(from: webAppLocalModel.logoUrl, for: newApp)
}
}
func downloadAndSaveImage(from urlString: String, for webApp: WebApp) async {
guard let url = URL(string: urlString) else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let localUrl = saveImageLocally(data: data) {
DispatchQueue.main.async {
#warning("this defer resolves issue awhen data is downloaded first time")
defer {
self.updateDownloadedAppsListFromCoreData()
// self.updateLists() //real main update
self.updateAvailableAppsListFilteredByDownloadedItems()//real main update
}
webApp.localLogoUrl = localUrl.absoluteString
do {
try self.context.save()
print("✅ Image saved in coreData at \(webApp.localLogoUrl!)")
} catch {
print("❌ Error saving local image URL: \(error)")
}
}
}
} catch {
print("❌ Error downloading the image: \(error)")
}
}
func saveImageLocally(data: Data) -> URL? {
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let fileName = UUID().uuidString + ".png" // Or the image format
let fileURL = documentsDirectory.appendingPathComponent(fileName)
do {
try data.write(to: fileURL)
print("OK file saved at: \(fileURL)")
return fileURL
} catch {
print("❌ Error saving the image locally: \(error)")
return nil
}
}
// MARK: filtered calls - for the store
/// availableAppsList must show only apps both in coredata and call response
private func updateAvailableAppsListFilteredByDownloadedItems() {
guard !downloadedAppsList.isEmpty else {
print("🔍 ❌ Not loading availableAppsList, even if there is no networking error, downloadedAppsList is empty")
return
}
defer {
print("🔍 ✅ updated availableAppsList, updateAvailableAppsListFilteredByDownloadedItems done")
}
// Grab preferred app in coreData
//----------------------------------------------------------------------
let fetchRequest: NSFetchRequest<WebApp> = WebApp.fetchRequest()
fetchRequest.sortDescriptors = []
var coreDataApps: [WebApp] = []
do {
coreDataApps = try persistenceController.container.viewContext.fetch(fetchRequest)
} catch {
print("Error fetching apps: \(error)")
}
//----------------------------------------------------------------------
// Create a Set of IDs from coreDataApps for more efficient search
let coreDataAppIDs = Set(coreDataApps.map { $0.id })
// Filter downloadedAppsList keeping only the apps
// whose ID is in the Set coreDataAppIDs
DispatchQueue.main.async {
self.availableAppsList = self.downloadedAppsList.filter { coreDataAppIDs.contains($0.id) }
self.test()
}
}
func test() {
guard !downloadedAppsList.isEmpty, !completePureValueFromCoreData.isEmpty else { return }
let downloadedAppsDict = Dictionary(uniqueKeysWithValues: downloadedAppsList.map { ($0.id, $0) })
availableAppsList = completePureValueFromCoreData.compactMap { localInCore in
guard var found = downloadedAppsDict[localInCore.id!] else { return nil }
found.localLogoUrl = localInCore.localLogoUrl
return found
}
print("test \(availableAppsList.first?.localLogoUrl ?? "❌")")
}
}
//*****************************************************************
import CoreData
import SwiftUI
// Used only to display data in lists
struct WebAppLocalModel: Codable, Identifiable, Equatable {
var id: String
var name: String
var logoUrl: String // This should be optional
var localLogoUrl: String?
// Initializer for CoreData objects, used to create the object you use when displaying data
init(webAppFromCoreData: WebApp) {
self.id = webAppFromCoreData.id ?? "N/A"
self.name = webAppFromCoreData.name ?? "N/A"
self.logoUrl = webAppFromCoreData.logoUrl ?? "N/A"
self.localLogoUrl = webAppFromCoreData.localLogoUrl ?? "N/A"
}
/// Initializer with specific parameters
/// - used only for mock response from the web
init(id: String = "N/A",
name: String = "N/A",
logoUrl: String = "N/A") {
self.id = id
self.name = name
self.logoUrl = logoUrl
}
}
//*****************************************************************
import SwiftUI
//here I should see only apps that are already in the download list, but also in the download list"
struct StoreAppView: View {
@EnvironmentObject var classFromEntryPoint: ClassFromEntryPoint
var body: some View {
NavigationView {
VStack {
if classFromEntryPoint.isLoading {
ProgressView("Loading...")
} else {
List {
ForEach(classFromEntryPoint.availableAppsList) { webAppLocalModel in
HStack {
if let localLogoUrl = webAppLocalModel.localLogoUrl,
let url = URL(string: localLogoUrl),
let imageData = try? Data(contentsOf: url),
let image = UIImage(data: imageData) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
} else {
Image("imgDefault")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
}
Text("\(webAppLocalModel.name)")
.font(.body)
.background(Color.green)
.onTapGesture {
print("Tapped: \(webAppLocalModel.name)")
}
}
}
}
.onAppear {
print("appeared")
// classFromEntryPoint.updateLists() //should not be required, view should upload via @Published
print("appeared downloadedAppsList - \(classFromEntryPoint.downloadedAppsList.count)")
print("appeared availableAppsList - \(classFromEntryPoint.availableAppsList.count)")
}
}
}
.navigationBarTitle("Available Apps")
}
}
}
发生这种情况是因为您的
saveImageLocally
函数将完整的绝对 URL 保存到文件中 - 但您需要的 URL 发生了变化。文档目录等位置的绝对路径包含 iOS 每次启动应用程序时都会更改的 UUID,因此文档目录路径每次都不同。如果您在打印文档目录路径的函数中添加 print
,您会看到这种情况发生。
由于您直接保存在文档目录中,因此您应该在 Core Data 中仅保存 文件名。也就是说,仅保存相对路径,在本例中是相对于文档目录的路径。当您想要使用照片时,查找文档目录 URL,然后向其中添加文件名以获得绝对路径。这样,您每次启动应用程序时都会获得正确的文档目录 URL。