为什么无法使用存储在磁盘上的图像(路径保存在 CoreData 上)读取或更新我的 UI

问题描述 投票:0回答:1

我下载(这里模拟)了一堆 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")
        }
    }
}
swiftui core-data file-manager
1个回答
0
投票

发生这种情况是因为您的

saveImageLocally
函数将完整的绝对 URL 保存到文件中 - 但您需要的 URL 发生了变化。文档目录等位置的绝对路径包含 iOS 每次启动应用程序时都会更改的 UUID,因此文档目录路径每次都不同。如果您在打印文档目录路径的函数中添加
print
,您会看到这种情况发生。

由于您直接保存在文档目录中,因此您应该在 Core Data 中仅保存 文件名。也就是说,仅保存相对路径,在本例中是相对于文档目录的路径。当您想要使用照片时,查找文档目录 URL,然后向其中添加文件名以获得绝对路径。这样,您每次启动应用程序时都会获得正确的文档目录 URL。

© www.soinside.com 2019 - 2024. All rights reserved.