Core data one to many relationship in swift

15,867

To address your questions directly:

I am using the data in a tableView. I have seen in some tutorials that the use: var vakken = [NSManagedObject]() and then use it in a tableview. Is this also possible with the model I have, and should I use it

It is certainly possible, but it would probably be better to use an NSFetchedResultsController (see the Apple Docs). This is specifically designed to make to easy to populate a tableView with data from Core-Data.

I first thought was to just save the data when the app terminates, so I would have the code in the app delegate, and then just retrive it when the app starts. How would I get the array to the appdelegate and would this be smart?

I wouldn't populate/save the array in the App Delegate. Some people follow Apple's template projects, which build the CoreData stack in the App Delegate; others have a separate class that they instantiate to manage the stack. From the look of your code, it currently uses the former. Your view controllers can then get the NSManagedObjectContext from the App Delegate using:

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context = appDelegate.managedObjectContext

(As beyowulf points out, you should not use AppDelegate().managedObjectContext as that creates a new instance, rather than referring to the existing instance). Once they have a context, your view controllers can fetch the data they need, add or update existing records, etc.

But how would I get from the result my array back or if I use NSManagedObject how can I refer to the attributes and in most particular the grades.

result is an array of NSManagedObjects. You can obtain the attribute values using valueForKey, the "read" equivalent of setValue:(_, forKey:):

let firstObject = result[0]
let firstObjectName = firstObject.valueForKey("name")

In the same way, the grades can be obtained with:

let firstObjectGrades = firstObject.valueForKey("grades")

There are better ways, though: see your final question below.

I also sort my array by name which can be A-Z or Z-A, and I sort by average which can be 10-1 or 1-10. I have the code to sort it in my array. But I don't know how to sort this with core data.

It is easiest to sort the data when you fetch it. To do this specify a NSSortDescriptor (see the Apple Docs) for the fetch:

let fetchRequest = NSFetchRequest(entityName: "Subject")
fetchRequest.sortDescriptors = [NSSortDescriptor(key:"name", ascending:true)

I have also seen a tutorial where they made a class for the attributes. I don't know if I need it nor how to use it?

Yes, use it. It will make life so much easier. Rather than write your own class definition, use the Xcode menu option "Create NSManagedObject subclasses" in the data model editor. It will create the class code for you and will also set each Entity to use the corresponding class. (If you wish to stick with your own code, you will need to amend the "class" for each entity in the data model editor).

Once you have the subclasses properly defined, you can then refer to the attributes and relationships using dot notation, rather than needing to use valueForKey and setValueForKey:

So:

let Subject = NSManagedObject(entity: entitySubject!, insertIntoManagedObjectContext: context)

will become:

let subject = Subject(entity: entitySubject!, insertIntoManagedObjectContext: context)

(Note, variables should begin with lowercase - Classes and Entity names begin with uppercase).

Grade.setValue(vakken.last!.cijfer[j], forKey: "cijfer")

becomes:

grade.cijfer = vakken.last!.cijfer[j]

and

let firstObjectGrades = firstObject.valueForKey("grades")

becomes:

let firstObjectGrades = firstObject.grades

Also, the addTagObject and removeTagObject functions will make it easier to manage the to-many relationship. Your code currently has:

Subject.setValue(NSSet(object: Grade), forKey: "grade")

This will replace any existing grades for that Subject with the Grade object: it doesn't add it to the existing. In fact, for one-many relationships it's far easier to manage the inverse (to-one) relationship:

grade.subject = subject

CoreData will handle the inverse (to-many) relationship automatically for you.

How can i delete one of the grades?

First, don't build separate arrays for each of the Grades attributes: just have one array for the Grades:

var vak: Subject! {
    didSet {
        // Update the view.
        grades = vak.grades.allObjects as! [Grade]
        self.tableView.reloadData()
    }
}

var grades: Array<Grade>!

You can get the attributes easily enough whenever you need them. For example, in your cellForRowAtIndexPath you might have something like this:

let grade = grades[indexPath.row]
let cijfer = grade.cijfer
let weging = grade.weging
// populate cell labels etc using "cijfer" and "waging"

Your commitEditingStyle can then easily find which Grade is being deleted, and handle accordingly:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        let grade = grades[indexPath.row]
        // either remove the Grade from your Subject (which leaves
        // the Grade "orphaned"):
        grade.subject = nil
        // or (probably better) completely delete the grade:
        managedobjectcontext.deleteObject(grade)
        // then remove it from the "grades" array:
        grades.removeAtIndex(indexPath.row)
        // and finally delete the corresponding table view row
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        // wrap up by saving the changes to the context:
        do{
            try mangedobjectcontext.save()
        }catch{
            print("error")
        }
    }
}

How can i save the vak(subject) to the context when i just change one of the variable?

Whenever you save the context, it saves ALL the changes to any objects registered with it (inserted into it or fetched using it). So, even if the only change you have made is to amend one attribute of one Subject, just call save() on the context.

Share:
15,867
Cing
Author by

Cing

Programming is just like placing domino's, one mistake can mess-up everything.

Updated on June 16, 2022

Comments

  • Cing
    Cing almost 2 years

    I have been searching for days on how to work with one to many relationships. But I can't get it figured out.

    I am working on a app which stores the grades of a subject. So one subject has a to-many relationship with grades. All the data is currently stored in a array.

    var vakken: [(naam:String, voorkeurGemiddelde:Double, wegingGemiddelde:Double, cijfer:[Double], weging:[Double], gemiddelde:Double)] = []
    Where the the [Double]'s - Cijfer and weging- are attributes of a grade.

    dataModel

    I currently have the following for saving the data:

     let context = AppDelegate().managedObjectContext
    
        // Create subject
        let entitySubject = NSEntityDescription.entityForName("Subject", inManagedObjectContext: context)
        let Subject = NSManagedObject(entity: entitySubject!, insertIntoManagedObjectContext: context)
    
        // Populate subject
        Subject.setValue(vakken.last!.naam, forKey: "name")
        Subject.setValue(vakken.last!.voorkeurGemiddelde, forKey: "voorkeurGemiddelde")
        Subject.setValue(vakken.last!.wegingGemiddelde, forKey: "wegingNP")
        Subject.setValue(vakken.last!.gemiddelde, forKey: "gemiddelde")
    
        for var j = 0; j < vakken.last!.cijfer.count-1; j++ {
    
            let Grade = NSEntityDescription.insertNewObjectForEntityForName("Grade", inManagedObjectContext: context)
    
            Grade.setValue(vakken.last!.cijfer[j], forKey: "cijfer")
            Grade.setValue(vakken.last!.weging[j], forKey: "weging")
    
            Subject.setValue(NSSet(object: Grade), forKey: "grade")
        }
    
        do{
            try context.save()
        }catch{
            print("error")
        }
    

    I am using the data in a tableView. I have seen in some tutorials that the use: var vakken = [NSManagedObject]() and then use it in a tableview.
    1. But is this also possible with the model I have, and should I use it?

    I first thought was to just save the data when the app terminates, so I would have the code in the app delegate, and then just retrive it when the app starts.

    2. But how would I get the array to the appdelegate and would this be smart?

    And for fetching the data I have this:

      let fetchRequest = NSFetchRequest(entityName: "Subject")
    
    //        // Add Sort Descriptor
    //        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)               
    //        fetchRequest.sortDescriptors = [sortDescriptor]
    
    
            let context = Appdelegate().managedObjectContext
    
            // Execute Fetch Request
            do {
                let result = try context.executeFetchRequest(fetchRequest)
                print(result)
            } catch {
                let fetchError = error as NSError
                print(fetchError)
            }
    

    3. But how would I get from the result my array back or if I use NSManagedObject how can I refer to the attributes and in most particular the grades.

    I also sort my array by name which can be A-Z or Z-A, and I sort by average which can be 10-1 or 1-10. I have the code to sort it in my array.
    4. But I don't know how to sort this with core data.

    I have also seen a tutorial where they made a class for the attributes link. So I made one my self:

        import UIKit
    import CoreData
    import Foundation
    
    class Subject: NSManagedObject {
        @NSManaged var gemiddelde: Double
        @NSManaged var name: String
        @NSManaged var voorkeurGemiddelde: Double
        @NSManaged var wegingNP: Double
        @NSManaged var Grades: NSSet
    }
    
    class Grade: NSManagedObject {
        @NSManaged var cijfer: Double
        @NSManaged var weging: Double
        @NSManaged var subject: Subject
    }
    
    extension Subject {
        func addTagObject(value:Grade) {
            let items = self.mutableSetValueForKey("grade");
            items.addObject(value)
        }
    
        func removeTagObject(value:Grade) {
            let items = self.mutableSetValueForKey("grade");
            items.removeObject(value)
        }
    }
    

    5. But I don't know if I need it nor how to use it?

    If you have any advise, tips, know good tutorials or know one of these questions. Your help would be appreciated.

    If you need any more information just let me know. :)

    Update:

    I have this code to get the object of the subject witch is clicked.

    if let indexPath = self.tableView.indexPathForSelectedRow {
                    let object = self.fetchedResultsController.objectAtIndexPath(indexPath)
                    let controller = segue.destinationViewController as! VakTableViewController
                    controller.vak = object as! Subject
    
                }
    

    And this is the code on the receiving end:

     var vak: Subject! {
        didSet {
            // Update the view.
            cijfers = vak.valueForKeyPath("grades.cijfer")!.allObjects as! [Double]
            wegingen = vak.valueForKeyPath("grades.weging")!.allObjects as! [Double]
            self.tableView.reloadData()
        }
    }
    
    var cijfers: Array<Double>!
    var wegingen: Array<Double>!
    

    6. But how can i delete on of the grades?

    I have tried this:

    // Override to support editing the table view.
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            if cijfers.count != 1 {
                let entityGrade = NSEntityDescription.entityForName("Grade", inManagedObjectContext: mangedobjectcontext)
                let grade = Grade(entity: entityGrade!, insertIntoManagedObjectContext: mangedobjectcontext)
    
                grade.cijfer = cijfers[indexPath.row]
                grade.weging = wegingen[indexPath.row]
                vak.removeGradeObject(grade)
                do{
                    try mangedobjectcontext.save()
                }catch{
                    print("error")
                }
                cijfers.removeAtIndex(indexPath.row)
                wegingen.removeAtIndex(indexPath.row)
                tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
                print(vak.valueForKeyPath("grades.cijfer")?.allObjects)
            }
        }
    }
    

    But this doesn't work.

    7. And how can i save the vak(subject) to the context when i just change own of the variable?

    like this: vak.gemiddelde = newgemiddelde

  • Cing
    Cing about 8 years
    Is it possible to get the cijfer of the grade in a subject because vak.grades seem to only be the relationship?
  • pbasdf
    pbasdf about 8 years
    Sure - revert to using valueForKeyPath: like this let cijfers = subject.valueForKeyPath("grades.cijfer").
  • Cing
    Cing about 8 years
    but how will i get individual numbers because cijfers[0] crashes
  • Cing
    Cing about 8 years
    print(cijfers) = {( 5.7, 6, 6.9 )}
  • pbasdf
    pbasdf about 8 years
    cijfers is an NSSet, so it's not possible to use an index. Convert it to an array using let cijfers = subject.valueForKeyPath("grades.cijfer").allObjects(), you can then use an index to get the individual values.
  • pbasdf
    pbasdf about 8 years
    @Cing Updated the answer.
  • Cing
    Cing about 8 years
    Just one final question how can i sort the grades array? like so: grades.sort()
  • pbasdf
    pbasdf about 8 years
    Replace this line: grades = vak.grades.allObjects as! [Grade] with this line:grades = vak.grades.sortedArrayUsingDescriptors([NSSortDescriptor(key‌​:"cijfer", ascending: true)]) as! [Grade] (assuming you wish to sort by cijfer).