简体   繁体   中英

CoreData: Unable to retrieve saved data after reopening the app

In my app I try to save a Team object.

To save this object I create a sheet with an add form and a Save button, these two views share the same ViewModel, the latter is initialized in the parent view of the sheet.

In my logic, I fetch the Teams saved in the database when I proceed to a save to refresh my view and see my Teams in a list.

Until then, there is no problem. However, when I close the app and restart it, if I go back to the list of my Teams, no item is displayed

I tried to debug this in every possible way, I don't see any anomaly, knowing that I have the same implementation of my CoreData logic in several apps that do not encounter this problem.

CoreDataStack:

class CoreDataStack {
    
    // MARK: - Singleton
    static let shared: CoreDataStack = CoreDataStack(modelName: "Boxscore")
    var persistentContainer: NSPersistentContainer
    
    // MARK: - Variables
    var mainContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    // MARK: - Init
    init(modelName: String, persistentStoreDescription: String? = "/dev/null") {
        let description = NSPersistentStoreDescription()
        if let persistentStoreDescription {
            description.url = URL(fileURLWithPath: persistentStoreDescription)
        }
        
        persistentContainer = NSPersistentContainer(name: modelName)
        persistentContainer.persistentStoreDescriptions = [description]
        persistentContainer.loadPersistentStores(completionHandler: { (_, error) in
            guard let unwrappedError = error else { return }
            fatalError("Unresolved error \(unwrappedError.localizedDescription)")
        })
        
    }
}

CoreDataManager:

class CoreDataManager {
    enum CDErrors: Error {
        case noData
        case saveError
    }
    
    let managedObjectContext: NSManagedObjectContext
    
    init(managedObjectContext: NSManagedObjectContext = CoreDataStack.shared.mainContext) {
        self.managedObjectContext = managedObjectContext
    }
    
    
    //MARK: Team
    func saveTeam(team: Team, completionHandler: @escaping (Result<BoxscoreTeam, CDErrors>) -> Void)  {
        DispatchQueue.main.async {
            let entity = BoxscoreTeam(context: self.managedObjectContext)
            entity.id = team.id
            entity.clubName = team.clubName
            entity.categorie = team.categorie?.rawValue
            entity.name = team.name
            entity.teamNumber = team.teamNumber
            
            do {
                try self.managedObjectContext.save()
                completionHandler(.success(entity))
            } catch {
                print(error)
                completionHandler(.failure(.saveError))
            }
        }
    }
    
    func fetchTeam(completionHandler: @escaping (Result<[BoxscoreTeam], CDErrors>) -> Void) {
        DispatchQueue.main.async {
            let request: NSFetchRequest = BoxscoreTeam.fetchRequest()
            
            do {
                let fetchedTeams = try self.managedObjectContext.fetch(request)
                return completionHandler(.success(fetchedTeams))
            } catch {
                print("Fetch teams fails with error : \(error.localizedDescription)")
                return completionHandler(.failure(.noData))
            }
        }
    }
}

AllTeamsView:

struct AllTeamsView: View {
    
    @ObservedObject public var viewModel: TeamViewModel = TeamViewModel()
    
    var body: some View {
        VStack {
//            if viewModel.fetchedTeams.isEmpty {
//                Text("No team regristred, add a new team")
//            } else {
                List {
                    ForEach(viewModel.fetchedTeams, id: \.id) { item in
                        ZStack {
                            NavigationLink(destination: TeamDetailsView(viewModel: viewModel, item: item)) {
                                    EmptyView()
                                }
                                .opacity(0.0)
                            TeamRowView(item: item)
                        }
                        .listRowInsets(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
                        .listRowSeparator(.hidden)
                    }
//                    .onDelete(perform: removeTeam)
                }
                .listStyle(InsetListStyle())
//            }
            Spacer()
        }
        .alert(viewModel.error,
                isPresented: $viewModel.showTeamError,
                actions: {
             Button("OK", role: .cancel) { viewModel.showTeamError = false }
         })
        .navigationBarTitleDisplayMode(.inline)
        .navigationTitle("All teams")
        .navigationBarItems(trailing:
                                Button {
            viewModel.showNewTeamSheet = true
        } label: {
            Image(systemName: "plus.circle")
                .tint(Color.subElement)
        })
        .sheet(isPresented: $viewModel.showNewTeamSheet) {
            NavigationView {
                NewTeamFormView(viewModel: viewModel)
            }
        }
    }
}

NewTeamFormView:

struct NewTeamFormView: View {
    
    @StateObject public var viewModel: TeamViewModel
    
    var body: some View {
        Form {
            Section {
                Picker("Select a categorie", selection: $viewModel.categorie) {
                    ForEach(Team.Categories.allCases, id: \.self) { cat in
                        Text(cat.rawValue)
                    }
                }
                .pickerStyle(.menu)
                
                Picker("Team gender", selection: $viewModel.teamGender) {
                    Text("Male").tag(0)
                    Text("Female").tag(1)
                }
                .pickerStyle(.segmented)
                
                Toggle("Categorie multiple teams ?", isOn: $viewModel.isMultipleTeams)
                
                if viewModel.isMultipleTeams {
                    TextField("Team number", text: $viewModel.teamNumber)
                }
            } header: {
                Text("Team information")
            }
        }
        .navigationTitle("New team")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarItems(trailing: Button(action: {
            viewModel.saveTeam()
        }, label: {
            Text("Save")
                .foregroundColor(Color.subElement)
        }))
    }
}

TeamViewModel:

public class TeamViewModel: ObservableObject {
    
    let coreDataManager: CoreDataManager = CoreDataManager(managedObjectContext: CoreDataStack.shared.mainContext)
    
    public var teamSamples: [Team] = [Team(categorie: .s, name: "U20-M", players: [], games: [], teamNumber: "2", score: 0, isMenTeam: true, isMultipleTeams: true)]
    
    @Published public var categorie: Team.Categories = .u11
    @Published public var teamGender: Int = 0
    @Published public var teamNumber: String = ""
    @Published public var isMultipleTeams: Bool = false
    
    @Published public var playerName: String = ""
    @Published public var playerNumber: String = ""
    
    @Published public var showNewTeamSheet: Bool = false
    @Published public var showNewPlayerSheet: Bool = false
    @Published public var showTeamError: Bool = false
    
    @Published public var error: String = ""
    
    @Published public var fetchedPlayers: [Player] = []
    @Published public var fetchedTeams: [Team] = []

    
    public var teamId: UUID = UUID()
    
    init() {
        self.fetchTeams()
    }
    
    public func fetchTeams() {
        self.fetchedTeams = []
        DispatchQueue.main.async {
            self.coreDataManager.fetchTeam { result in
                switch result {
                case .success(let teams):
                    teams.forEach { bsTeam in
                        let team = Team(id: bsTeam.id ?? UUID(),
                                        clubName: bsTeam.clubName ?? "",
                                        categorie: Team.Categories(rawValue: bsTeam.categorie ?? "") ?? .s,
                                        name: bsTeam.name ?? "",
                                        players: self.fetchedPlayers.filter({ $0.teamId == self.teamId }),
                                        games: nil,
                                        teamNumber: bsTeam.teamNumber)
                        
                        self.fetchedTeams.append(team)
                    }
                case .failure(let error):
                    self.showTeamError = true
                    self.error = error.localizedDescription
                }
            }
        }
    }
    
    public func saveTeam() {
        DispatchQueue.main.async {
            let team = Team(categorie: self.categorie,
                            name: "\(self.categorie.rawValue) - \(self.teamGender == 0 ? "M" : "F") \(self.isMultipleTeams ? self.teamNumber : "")",
                            players: [],
                            games: [],
                            teamNumber: self.teamNumber,
                            score: 0,
                            isMenTeam: self.teamGender == 0,
                            isMultipleTeams: self.isMultipleTeams)
            
            self.coreDataManager.saveTeam(team: team, completionHandler: { result in
                switch result {
                case .success:
                    DispatchQueue.main.async {
                        self.fetchTeams()
                        self.showNewTeamSheet = false
                    }
                case .failure(let error):
                    self.showTeamError = true
                    self.error = error.localizedDescription
                }
            })
        }
    }
}

I've been running on it for two days now, if anyone sees an anomaly I'm all ears.

EDIT: The problem was in CoreDataStack, see the new implementation below

    class CoreDataStack {
        
        // MARK: - Singleton
        static let shared: CoreDataStack = CoreDataStack(modelName: "Boxscore")
        var persistentContainer: NSPersistentContainer
        
        // MARK: - Variables
        var mainContext: NSManagedObjectContext {
            return persistentContainer.viewContext
        }
    
        // MARK: - Init
        init(modelName: String, persistentStoreDescription: String? = nil) {
            persistentContainer = NSPersistentContainer(name: modelName)
            
            if let psd = persistentStoreDescription {
                let description = NSPersistentStoreDescription()
                description.url = URL(fileURLWithPath: psd)
                persistentContainer.persistentStoreDescriptions = [description]
            }
            
            persistentContainer.loadPersistentStores(completionHandler: { (_, error) in
                guard let unwrappedError = error else { return }
                fatalError("Unresolved error \(unwrappedError.localizedDescription)")
            })
            
        }
    }

/dev/null as a file URL for your core data stack creates an in-memory SQLite store, which is excellent for unit tests and SwiftUI previews for your core data model, but not so great for actual persistence. Using that path should not be the default, it should be an opt-in thing for the use cases mentioned above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM