0
votes

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)")
    }
}
2
var goal: Goal! declares the variable but it is not setvadian
I'm passing the value from the previous screen, vc.goal = fetchedResultsController.object(at: indexPath), and using it as the predicate.Kevvv
Sorry, my bad, where do you run the fetch code in DetailViewController? And rather than try? catch the error.vadian
No worries. I run the fetch code in viewDidLoad().Kevvv

2 Answers

1
votes

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)

0
votes

I figured out that it's due to Core Data Fault where Core Data lazy loads the data and unless you explicitly access the data, the value will not be displayed.

You can either do something like the following:

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.goalTitle = goal.title
    vc.date = progress.date
    
    if let comment = progress.comment {
        vc.comment = comment
    }
    
    navigationController?.pushViewController(vc, animated: true)
}

or setreturnsObjectsAsFaults to false.

Here's a good article on the topic.