WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: CoreData entities

Forums > SwiftUI

Hi all! I have a simple to-do list app which until now consisted of two entities: Project > Task. That worked fine for me but now I wanted wanted to put another Entity in-between: Project > Module > Task. My issue now is that each task I add will be added to ALL Modules instead of the one I'm currently in. Would anyone be able to push me into the right direction?

Here is my code:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(entity: Project.entity(),
                  sortDescriptors: [
                    NSSortDescriptor(keyPath: \Project.deadline, ascending: true),
                    NSSortDescriptor(keyPath: \Project.dueDate, ascending: true)])
    var projects: FetchedResults<Project>

    @State private var showingAddProject = false

    var body: some View {
        NavigationView {
            ZStack {
                Form {
                    ForEach(projects, id: \.self) { project in
                        Section(header: dueDate1()) {
//                            NavigationLink(destination: ProjectDetailView(project: project)) { <<< This is the old NavigationLink from when I only had two Entities.
                            NavigationLink(destination: Modules(project: project)) {
                                VStack (alignment: .leading){
                                    HStack {
                                        Text(project.wrappedName)
                                            .font(.headline)
                                        Text("(\(project.wrappedType))")
                                    }
                                }
                            }
                        }
                    }
                    .onDelete(perform: deleteProjects)
                }
                VStack {
                    Spacer()
                    HStack {
                        Spacer()
                        Button {
                            self.showingAddProject.toggle()
                        } label: {
                            Image(systemName: "plus.circle.fill")
                                .font(.system(size: 60))
                                .frame(width: 70, height: 70)
                                .foregroundColor(.blue)
                        }
                    }.padding(.horizontal)
                }
                .navigationTitle("Roadmaps")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        EditButton ()
                    } 
                }.sheet(isPresented: $showingAddProject, content: {
                    AddProjectView().environment(\.managedObjectContext, self.moc)
                })
            }
        }
    }

    func deleteProjects(at offsets: IndexSet) {
        for offset in offsets {
            let project = projects[offset]
            moc.delete(project)
        }
        try? moc.save()
    }
}

struct ContentView_Previews: PreviewProvider {
    static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    static var previews: some View {
        let module = Module(context: moc)
        ContentView()
    }
}
struct Modules: View {

    @Environment(\.managedObjectContext) var moc
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var project: Project
    @State private var name = ""
    @State private var showingAddModule = false

    var body: some View {
        Form {
            Section(header: Text("Modules")) {
                ForEach(project.modulesArray) { module in
                    NavigationLink(destination: TaskView(project: project)) {
                        HStack {
                            if module.isFinished {
                                Image(systemName: "checkmark.circle")
                                    .foregroundColor(.green)
                               } else if module.hasPriority {
                                    Image(systemName: "exclamationmark.triangle")
                                       .foregroundColor(.red)
                               } else {
                                    Image(systemName: "circle")
                                       .foregroundColor(.orange)
                               }
                            VStack(alignment: .leading, spacing: 5) {
                                Text(module.wrappedName)
                                    .fontWeight(.semibold)
                                .padding(.leading, 10)
                            }
                        }
                    }
                }.onDelete(perform: deleteModule)
            }
        }.navigationTitle(project.wrappedName)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        self.showingAddModule.toggle()
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }.sheet(isPresented: $showingAddModule, content: {
                AddModuleView(project: self.project).environment(\.managedObjectContext, self.moc)
            })
    }

    func deleteModule(at offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                let module = project.modulesArray[index]
                moc.delete(module)
                PersistenceController.shared.save()
            }
        }
    }    
}

struct Modules_Previews: PreviewProvider {
    static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

    static var previews: some View {
        let project = Project(context: moc)
        project.projectName = "Test Project"

        let module = Module(context: moc)
        return NavigationView {
            Modules(project: project)
        }
    }
}
struct TaskView: View {
    @Environment(\.managedObjectContext) var moc
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var project: Project

    @State private var taskName: String = ""
    @State private var showingProjectEditView = false
    @State private var showingAddTask = false
    @State var finished = false
    @State private var category = "General"

    var categories = ["General", "Blue", "Green"]

    var body: some View {
        VStack {
            List {
                Section(header: Text("Tasklist")) {
                    ForEach(project.tasksArray) { task in
                        NavigationLink(destination: EditTaskView(project: project, task: task)) {
                            HStack {
                                Image(systemName: task.finished ? "checkmark.circle" : "circle")
                                    .foregroundColor(task.finished ? .green : .red)
                                    Text(task.wrappedName)
                                        .fontWeight(.semibold)
                                    .padding(.leading, 10)
                            } 
                        }
                    }.onDelete(perform: deleteTask)
                    }
            }
            .listStyle(InsetGroupedListStyle())
            .navigationTitle("Tasks")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        self.showingProjectEditView.toggle()
                    }) {
                        Image(systemName: "pencil")
                    }
                }
            }.sheet(isPresented: $showingProjectEditView, content: {
                EditProjectView(project: self.project).environment(\.managedObjectContext, self.moc)
            })
            VStack {
            HStack {
                TextField("Enter Task", text: $taskName)
                    .modifier(TextFieldClearButton(text: $taskName))
                    .textFieldStyle(.roundedBorder)
                    .padding(.leading)
                    .frame(minHeight: CGFloat(30))
                Button {
                    if !taskName.isEmpty {
                        addTask()
                    }
                } label: {
                    Image(systemName: "arrow.up.circle.fill")
                        .font(.largeTitle)
                }
                Spacer()
            }
            .padding(.bottom)
            }
        }
    }

    func addTask() {
        let newTask = Task(context: moc)
        newTask.taskName = taskName
        newTask.finished = false

        project.addToTasks(newTask)
        PersistenceController.shared.save()
        self.taskName = ""
        hideKeyboard()
    }

    func deleteTask(at offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                let task = project.tasksArray[index]
                moc.delete(task)
                PersistenceController.shared.save()
            }
        }
    }
}

#if canImport(UIKit)
extension View {
    func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
#endif

struct ProjectDetailView_Previews: PreviewProvider {
    static var previews: some View {
        let moc = PersistenceController.preview.container.viewContext
        let newProject = Project(context:  moc)
        newProject.projectName = "Apple"

        let task1 = Task(context: moc)
        task1.taskName = "Blue"

        let task2 = Task(context: moc)
        task2.taskName = "Green"

        newProject.addToTasks(task1)
        newProject.addToTasks(task2)

        return NavigationView {
            TaskView(project: newProject)
                .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
        }
    }
}

Saving Projects, Modules per Project works fine. Just Task will get added to all Modules. All possible hints are much appreciated.

Many Thanks!

   

I don't see your data model. But my first guess from project.addToTasks(newTask) is that you have the tasks in the hierarchy under Project and not under Module.

   

Thanks @Hatsushira. Where exactly would I determine the hierarchy?

Right, I forgot the DataModel. I'll write it down here as I don't know what would be the best way to copy/display it here:

Entity Project

Attributes: projectName id type finished

Relationships Module Task


Entity Module

Attributes: name isFinished hasPriority creationDate

Relationsships Project Task


Entity Task

Attributes: taskName priority finished

Relationships Module Project

Let me know if I can include the model in a better way!

   

You don't need a relationship Project > Task. You only need Project > Module and Module > Task. If you want to show all Tasks from a Project you can loop over the Modules and their Tasks. That's what I would do.

   

That's what I also thought but as soon as I delete the relationship Project > Task the app crashes.

I don't really understand the error message:

Exception NSException * "-[Project tasks]: unrecognized selector sent to instance 0x600002889bd0" 0x0000600000492e20

Is there anything else I need to tell Xcode or CoreData when I delete the relationsship?

   

You should clean your build folder before building as well. Sometimes the automatically created classes for Core Data mess up.

If you created your Core Data classes with the built in tool you have to rebuild them as well. On the other hand, you have to delete projects.addToTasks lines in your code as well as they are only exist when you have an relationship Project > Task. Looking at the error message this is likely the problem. Selector is usually the word used when calling a method which doesn't exist.

1      

Many thanks @Hatsushira!

It took a while because more errors got thrown but I finally managed to fix everything thanks to your help!

1      

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.