Search code examples
iosswiftcore-datanspredicatensmanagedobject

How to use a predicate for one-to-many managed objects in Core Data


I currently have two managed objects for Core Data that has one-to-many relationship.

Goal

extension Goal {

    @nonobjc public class func createFetchRequest() -> NSFetchRequest<Goal> {
        return NSFetchRequest<Goal>(entityName: "Goal")
    }

    @NSManaged public var title: String
    @NSManaged public var date: Date
    @NSManaged public var progress: NSSet?

}

Progress

extension Progress {

    @nonobjc public class func createFetchRequest() -> NSFetchRequest<Progress> {
        return NSFetchRequest<Progress>(entityName: "Progress")
    }

    @NSManaged public var date: Date
    @NSManaged public var comment: String?
    @NSManaged public var goal: Goal

}

For every goal, you can have multiple Progress objects. The problem is when I request a fetch for Progress with a particular Goal as the predicate, nothing is being returned. I have a suspicion that I'm not using the predicate properly.

This is how I request them.

  1. First, I fetch Goal for a table view controller:
var fetchedResultsController: NSFetchedResultsController<Goal>!

if fetchedResultsController == nil {
    let request = Goal.createFetchRequest()
    let sort = NSSortDescriptor(key: "date", ascending: false)
    request.sortDescriptors = [sort]
    request.fetchBatchSize = 20
    
    fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: "title", cacheName: nil)
    fetchedResultsController.delegate = self
}

fetchedResultsController.fetchRequest.predicate = goalPredicate

do {
    try fetchedResultsController.performFetch()
} catch {
    print("Fetch failed")
}
  1. And pass the result to the next screen, Detail view controller:
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
    vc.goal = fetchedResultsController.object(at: indexPath)
    navigationController?.pushViewController(vc, animated: true)
}
  1. Finally, I fetch Progress using the Goal as the predicate from Detail view controller:
var goal: Goal!
let progressRequest = Progress.createFetchRequest()
progressRequest.predicate = NSPredicate(format: "goal == %@", goal)

if let progress = try? self.context.fetch(progressRequest) {
    print("progress: \(progress)")

    if progress.count > 0 {
        fetchedResult = progress[0]
        print("fetchedResult: \(fetchedResult)")
    }
}

Goal is being returned properly, but I get nothing back for Progress. I've tried:

progressRequest.predicate = NSPredicate(format: "goal.title == %@", goal.title)

or

progressRequest.predicate = NSPredicate(format: "ANY goal == %@", goal)

but still the same result.

Following is how I set up the relationship:

// input for Progress from the user
let progress = Progress(context: self.context)
progress.date = Date()
progress.comment = commentTextView.text

// fetch the related Goal
var goalForProgress: Goal!
let goalRequest = Goal.createFetchRequest()
goalRequest.predicate = NSPredicate(format: "title == %@", titleLabel.text!)

if let goal = try? self.context.fetch(goalRequest) {
    if goal.count > 0 {
        goalForProgress = goal[0]
    }
}

// establish the relationship between Goal and Progress
goalForProgress.progress.insert(progress)

// save
if self.context.hasChanges {
    do {
        try self.context.save()
    } catch {
        print("An error occurred while saving: \(error.localizedDescription)")
    }
}

Solution

  • Actually you don't need to refetch the data. You can get the progress from the relationship

    • Declare progress as native Set

      @NSManaged public var progress: Set<Progress>
      
    • In DetailViewController delete the fetch code in viewDidLoad and declare

      var progress: Progress!
      
    • In the first view controller filter the progress

      let goal = fetchedResultsController.object(at: indexPath)
      if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
         let progress = goal.progress.first(where: {$0.goal.title == goal.title}) {
          vc.progress = progress
          navigationController?.pushViewController(vc, animated: true)
      }
      

    And consider to name the to-many relationship in plural form (progresses)