Home > Software engineering >  Storing Core Data asynchronously breaks relationsip
Storing Core Data asynchronously breaks relationsip

Time:09-14

I am trying to build a Swift app for iOS. I am having a simple data model, an entity GameModule and another entity GameModuleFile with relation one to many (one GameModule to many GameModuleFile).

I load a zip file from a disk, unzip it and populate the files into GameModuleFile entity with relation to the last module.

If I run it synced it works perfect. But if I run it async, the files are not stored into the Core Data with the relation. The code still runs, gives no errors, but the relationship is not there.

Actually, to have it more funny, the relationship is there ok for small zip files (hundreds of files), but it is not there for large files (thousands of files).

Any help?

import SwiftUI
import UniformTypeIdentifiers

struct GameModuleListView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: [SortDescriptor(\.order)]) var gameModules: FetchedResults<GameModule>
    @State private var loadModuleTrigger = false

    var body: some View {
        NavigationView {
            List {
                ForEach(gameModules, content: GameModuleRow.init)
            }
        }
            .navigationViewStyle(StackNavigationViewStyle())
            .fileImporter(isPresented: $loadModuleTrigger, allowedContentTypes: [.zip], allowsMultipleSelection: false, onCompletion: loadModule)
    }
    
    func loadModule(res: Result<[URL], Error>) {
        let url = try! res.get()[0]
        DispatchQueue.global(qos: .userInitiated).async {
            try? gameModules[gameModules.count - 1].loadFiles(context: moc, url: url)
        }
    }
}

import ZIPFoundation

extension GameModule {
    func loadFiles(context moc: NSManagedObjectContext, url: URL) throws {
        let archive = try GameModule.getArchiveFromUrl(url: url)
        for entry in archive {
            if entry.path.last! != "/" {
                _ = try? archive.extract(entry) { data in
                    let file = GameModuleFile(context: moc)
                    file.filePath = entry.path
                    file.blob = data
                    file.ownerModule = self
                }
            }
        }
        try? moc.save()
    }
}

Update

Based on @cgontijo advice down there and some googling, I modified the code using background context to load the files into Core Data. This is working now.

import SwiftUI
import UniformTypeIdentifiers

struct GameModuleListView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: [SortDescriptor(\.order)]) var gameModules: FetchedResults<GameModule>
    @State private var loadModuleTrigger = false

    var body: some View {
        NavigationView {
            List {
                ForEach(gameModules, content: GameModuleRow.init)
            }
        }
            .navigationViewStyle(StackNavigationViewStyle())
            .fileImporter(isPresented: $loadModuleTrigger, allowedContentTypes: [.zip], allowsMultipleSelection: false, onCompletion: loadModule)
    }
    
    func loadModule(res: Result<[URL], Error>) {
        moc.perform {
            let newModule = try! GameModule(moc)
            let url = try! res.get()[0]
            try? newModule.loadFiles(context: moc, url: url)
            try? moc.save()
        }
    }
}

import ZIPFoundation

extension GameModule {
    func loadFiles(context moc: NSManagedObjectContext, url: URL) throws {
        let archive = try GameModule.getArchiveFromUrl(url: url)
        for entry in archive {
            if entry.path.last! != "/" {
                _ = try? archive.extract(entry) { data in
                    let file = GameModuleFile(context: moc)
                    file.filePath = entry.path
                    file.blob = data
                    file.ownerModule = self
                }
            }
        }
    }
}

CodePudding user response:

The problem might be due to your loadFiles function running on a background queue and the managedObjectContext was initialised to be used with the main queue.

You must only access Managed Objects on the dispatch queue associated with the context.

Try adding this launch argument to your scheme:

-com.apple.CoreData.ConcurrencyDebug 1

With this argument, the app will throw an exception if you mistakenly access a managed object on the wrong queue.

If that is the case, you need to create a second managedObjectContext associated with a background thread for background processing.

You can find more details in this link: https://developer.apple.com/documentation/coredata/using_core_data_in_the_background

  • Related