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.