Let's say I have two entities, Employee and Department. A department has a to-many relationship with an employee, many employees can be in each department but each employee only belongs to one department. I want to display all of the employees in a table view sorted by data that is a property of the department they belong to using an NSFetchedResultsController. The problem is that I want my table to update when a department object receives changes just like it does if the regular properties of employee change, but the NSFetchedResultsController doesn't seem to track related objects. I've gotten passed this issue partially by doing the following:
for (Employee* employee in department.employees) {
[employee willChangeValueForKey:#"dept"];
}
/* Make Changes to department object */
for (Employee* employee in department.employees) {
[employee didChangeValueForKey:#"dept"];
}
This is obviously not ideal but it does cause the employee based FRC delegate method didChangeObject to get called. The real problem I have left now is in the sorting a FRC that is tracking employee objects:
NSEntityDescription *employee = [NSEntityDescription entityForName:#"Employee" inManagedObjectContext:self.managedObjectContext];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"department.someProperty" ascending:NO];
This works great and sorts the employees correctly the first time it's called, the problem is that when I make changes to some property of a department that should change the sorting of my employee table, nothing happens. Is there any nice way to have my employee FRC track changes in a relationship? Particularly I just need some way to have it update the sorting when the sort is based on a related property. I've looked through some similar questions but wasn't able to find a satisfactory solution.
The NSFetchedResultsController is only designed to watch one entity at a time. Your setup, while it makes sense, it a bit beyond what the NSFetchedResultsController is currently capable of watching on its own.
My recommendation would be to set up your own watcher. You can base it off the ZSContextWatcher I have set up on GitHub, or you can make it even more straightforward.
Basically, you want to watch for NSManagedObjectContextDidSaveNotification postings and then reload your table when one fire that contains your department entity.
I would also recommend filing a rdar with Apple and asking for the NSFetchedResultsController to be improved.
Swift
Because the NSFetchedResultsController is designed for one entity at a time, you have to listen to the NSManagedObjectContextObjectsDidChangeNotification in order to be notified about all entity relationship changes.
Here is an example:
//UITableViewController
//...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChangeHandler(notification:)), name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
#objc fileprivate func managedObjectsDidChangeHandler(notification: NSNotification) {
tableView.reloadData()
}
//...
This is a known limitation of NSFetchedResultsController: it only monitors the changes of you entity's properties, not of its relationships' properties. But your use case is totally valid, and here is how to get over it.
Working Principle
After navigating a lot of possible solutions, now I just create two NSFetchedResultsController: the initial one (in your case, Employee), and another one to monitor the entities in the said relationship (Department). Then, when a Department instance is updated in the way it should update your Employee FRC, I just fake a change of the instances of affiliated Employee using the NSFetchedResultsControllerDelegate protocol. Note that the monitored Department property must be part of the NSSortDescriptors of its NSFetchedResultsController for this to work.
Example code
In your example if would work this way:
In your view controller:
var employeesFetchedResultsController:NSFetchedResultsController!
var departmentsFetchedResultsController:NSFetchedResultsController!
Also make sure you declare conformance to NSFetchedResultsControllerDelegate in the class declaration.
In viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
// [...]
employeesFetchedResultsController = newEmployeesFetchedResultsController()
departmentsFetchedResultsController = newDepartmentsFetchedResultsController()
// [...]
}
In the departmentsFetchedResultsController creation:
func newDepartmentsFetchedResultsController() {
// [specify predicate, fetchRequest, etc. as usual ]
let monitoredPropertySortDescriptor:NSSortDescriptor = NSSortDescriptor(key: "monitored_property", ascending: true)
request.sortDescriptors = [monitoredPropertySortDescriptor]
// continue with performFetch, etc
}
In the NSFetchedResultsControllerDelegate methods:
That's where the magic operates:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if controller == departmentsFetchedResultsController {
switch(type){
case .insert, .delete, .update:
managedObjectContext.performAndWait {
let department = anObject as! Department
for employee in (department.employees ?? []) {
// we fake modifying each Employee so the FRC will refresh itself.
let employee = employee as! Employee // pure type casting
employee.department = department
}
}
break
default:
break
}
}
}
This fake update of the department of each impacted employee will trigger the proper update of employeesFetchedResultsController as expected.
SwiftUI
I haven't seen posts that directly addressed this issue in SwiftUI. After trying solutions outlined in many posts, and trying to avoid writing custom controllers, the single factor that made it work in SwiftUI—which was part of the previous post from harrouet (thank you!)—is:
Make use of a FetchRequest on Employee.
If you care about, say, the employee count per department, the fake relationship updates did not make a difference in SwiftUI. Neither did any willChangeValue or didChangeValue statements. Actually, willChangeValue caused crashes on my end.
Here's a setup that worked:
import CoreData
struct SomeView: View {
#FetchRequest var departments: FetchedResults<Department>
// The following is only used to capture department relationship changes
#FetchRequest var employees: FetchedResults<Employee>
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: employees.count)
}
}
//.id(employees.count) does not trigger view updates
}
}
struct DepartmentView: View {
var department: Department
// Not used, but necessary for the department view to be refreshed upon employee updates
var totalEmployeeCount: Int
var body: some View {
// The department's employee count will be refreshed when, say,
// a new employee is created and added to the department
Text("\(department) has \(department.employees.count) employee(s)")
}
}
I don't know if this fixes all the potential issues with CoreData relationships not propagating to views, and it may present efficiency issues if the number of employees is very large, but it worked for me.
An alternative that also worked for establishing the right employee count without grabbing all employees—which may address the efficiency issue of the above code snippet—is to create a view dependency on a NSFetchRequestResultType.countResultType type of FetchRequest:
// Somewhere in a DataManager:
import CoreData
final class DataManager {
static let shared = DataManager()
let persistenceController: PersistenceController
let context: NSManagedObjectContext!
init(persistenceController: PersistenceController = .shared) {
self.persistenceController = persistenceController
self.context = persistenceController.container.viewContext
}
func employeeCount() -> Int {
var count: Int = 0
context.performAndWait {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Employee")
fetchRequest.predicate = nil
fetchRequest.resultType = NSFetchRequestResultType.countResultType
do {
count = try context.count(for: fetchRequest)
} catch {
fatalError("error \(error)")
}
}
return count
}
}
And the main View becomes:
import CoreData
struct SomeView: View {
#FetchRequest var departments: FetchedResults<Department>
// No #FetchRequest for all employees
var dataManager = DataManager.shared
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: dataManager.employeeCount())
}
}
//.id(dataManager.employeeCount()) does not trigger view updates
}
}
// DepartmentView stays the same.
Again, this may not resolve all possible relationship dependencies, but it gives hope that view updates can be prompted by considering various types of FetchRequest dependencies within the SwiftUI views.
A note that DataManager needs NOT be an ObservableObject being observed in the View for this to work.
Related
How can I use #FetchRequest in SwiftUI with a fetch request based on a variable being passed in from a parent view, while also ensuring that the view updates based on Core Data changes?
I have this issue where using #FetchRequest in SwiftUI while passing in a variable to the fetch request doesn't work. It results in the following error:
Cannot use instance member 'name' within property initializer; property initializers run before 'self' is available
struct DetailView: View {
#FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
}
extension Report {
#nonobjc public class func fetchRequest(name: String) -> NSFetchRequest<Report> {
let fetchRequest = NSFetchRequest<Report>(entityName: "Report")
fetchRequest.fetchLimit = 1
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Report.createdAt, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "details.name == %#", name)
return fetchRequest
}
}
This view is pushed using a NavigationLink from a previous view. Like: NavigationLink(destination: DetailView(name: item.name)).
I've looked at this answer and I don't think that it will work for my use case since normally the #FetchRequest property wrapper automatically listens for data changes and updates automatically. Since I'm doing an async call to make a network request and update Core Data, I need the data variable to update dynamically once that is complete.
I've also looked at this answer but I think it has some of the same problems as mentioned above (although I'm not sure about this). I'm also getting an error using that code on the self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default) line. So I'm not sure how to test my theory here.
Cannot assign to property: 'data' is a get-only property
struct DetailView: View {
#FetchRequest
private var data: FetchedResults<Report>
let name: String
var body: some View {
Text("\(data.first?.summary.text ?? "")")
.navigationTitle(name)
.onAppear {
ReportAPI.shared.fetch(for: name) // Make network request to get Report data then save to Core Data
}
}
init(name: String) {
self.name = name
self.data = FetchRequest(fetchRequest: Report.fetchRequest(name: name), animation: .default)
}
}
Therefore, I don't think that question is the same as mine, since I need to be sure that the view updates dynamically based on Core Data changes (ie. when the fetch method saves the new data to Core Data.
I am trying to update an attribute in my Core Data through an NSManagedObject. As soon as I update it, I save the context and it gets saved successfully.
Problem
After the context saves it, the UI (SwiftUI) won't update it with the new value. If I add a completely new Children into Core Data (insert) the UI gets updated.
What I tried:
Asperi approach - I can print out the correct new value in .onReceive but the UI doesn't update
Using self.objectWillChange.send() before context.save() - didn't work either
Changed the Int16 to String, because I was speculating that somehow Int16 is not observable? Didn't work either
Is this a SwiftUI bug?
As-is State
//only updating the data
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchReq = NSFetchRequest<Card>(entityName: "Card") //filter distributorID; should return only one Card
fetchReq.resultType = .managedObjectResultType
fetchReq.predicate = NSPredicate(format: "id == %#", params["id"]!) //I get an array with cards
var cards :[Card] = []
cards = try context.fetch(fetchReq)
//some other functions
cards[0].counter = "3" //
//...
self.objectWillChange.send() //doesn't do anything?
try context.save()
//sucessfully, no error. Data is there if I restart the app and "force" a reload of the UI
//Core Data "Card"
extension Card :Identifiable{
#nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
//...
#NSManaged public var id: String
//....
}
//Main SwiftUI - data is correctly displayed on the card
#FetchRequest(entity: Card.entity(),
sortDescriptors: [],
predicate: nil)
var cards: FetchedResults<Card>
List {
ForEach(cards){ card in
CardView(value: Int(card.counter)!, maximum: Int(card.maxValue)!,
headline: card.desc, mstatement: card.id)
}
If first code block is a part of ObservableObject, then it does not look that third block, view one, depends on it, and if there is no changes in dependencies, view is not updated.
Try this approach.
But if there are dependencies, which are just not provided then change order of save and publisher as
try context.save()
self.objectWillChange.send()
The issue being faced is my inability to access an entity's attributes when it is a relationship to the entity I am fetching.
In relation to my app, I am creating a fitness tracking app, I have a detail table view controller with my tracked activities. When I tap a cell the view segues into another View Controller to display a map of the tracked locations.
Using the fetched results controller I am fetching "Entity1". When I tap a cell 'I think' I want to segue & pass "Entity2" attribute values into another view controller. Except the relationship from "Entity1" to "Entity2" is a "To Many" relationship and in the core data properties for "Entity1",
extension Entity1
{
// instead of "Entity2" being represented as
// #NSManaged var entity2: Entity2?
//it is represented as
#NSManaged var entity2: NSOrderedSet?
}
thus i can not access "Entity2" properties.
How I fetch "Entity1":
func fetchEntity1ResultsController( _ context: NSManagedObjectContext )
{
let request: NSFetchRequest<Entity1> = NSFetchRequest( entityName: "Entity1" )
request.sortDescriptors = [ NSSortDescriptor( key: "timestamp", ascending: false ) ]
fetchedResultsController = NSFetchedResultsController( fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil )
fetchedResultsController?.delegate = self
do
{
try fetchedResultsController?.performFetch()
}
catch
{
print("Couldn't fetch results controller")
}
}
How I would attempt to access "Entity2" properties.
private func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
guard let entity1 = fetchedResultsController?.object(at: indexPath) else { return }
entity1.entity2.//No core data properties listed here
}
To conclude: My question is - How can I tap a cell that is the "Entity1" description where "Entity2" is in relationship to and access "Entity2" attribute values to display that data in my map view controller.
Since this is a to-many relationship, what you're calling entity2 is a set of multiple instances of Entity2. That's what the "many" part of "to-many" means here-- one Entity1 is related to a collection of multiple Entity2 instances. You can't access Entity2 attributes on a set, because it's a collection of more than one instance. To access Entity2 attributes you first need to select one object from the set.
How you do that depends on how your app is supposed to work. You have an NSOrderedSet, and it has a variety of options for selecting one of the objects it contains. You could ask for the first object, or the last one, or the Entity2 at a specific location in the ordered set. There are other options besides these; see the NSOrderedSet documentation for more info on them.
The question i have is in regards to a Core Data one-to-many relationship as of right now i have my app being able to let the user input employee information and saving to core data, then updating the employee table view. The problem i face is the relationship between the employee and delivery. Im currently trying to display a specific employees deliveries when clicking on an employee in the employee table view. After selecting an employee from the employee tableView i want it to segue to another tableview and display the employees deliveries in another UITableview.
What I'm trying to Accomplish:
1) Display Specific Employee's Deliveries
2) Add deliveries to the NSSet
Here are my two managedObjects
extension Delievery {
#NSManaged var address: String?
#NSManaged var phoneNumber: String?
#NSManaged var subTotal: NSNumber?
#NSManaged var total: NSNumber?
#NSManaged var employee: Employee?
}
extension Employee {
#NSManaged var first: String?
#NSManaged var last: String?
#NSManaged var phoneNumber: String?
#NSManaged var wage: NSNumber?
#NSManaged var id: NSNumber?
#NSManaged var delievery: NSSet?
}
how i prepared for segue from employeeTableViewController to deliveryTableViewContorller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue == "DelieverySegue"{
let employeeDeliveries = segue.destinationViewController as! DelieveryTableViewController
let indexPath = self.tableView.indexPathForSelectedRow
let selectedEmployee = employees[indexPath!.row]
employeeDeliveries.employee = selectedEmployee
}
}
The variables of the deliveryTableViewController are
var employee: NSManagedObject!
var deliveries : [NSManagedObject]!
var managedObjectContext : NSManagedObjectContext!
In this photo it shows the rest of my deliveryTableViewController the problem i have is how do i return the amount of deliveries for a specific employee in the numberOfRowsInSection function and how do i fetch the deliveries of the employee.
deliveryTableViewController set up
In this last photo my question is how to i add this delivery entry to the employee selected? this is how I'm currently saving my delivery entries
how i save my delivery entries
If you made it this far i appreciate you looking through my problem. If anyone can help me the slightest with this issue i'd appreciate it if you feel i've left some information out that is needed please let me know.
UPDATE:
Here is the picture of the DelieveryTableViewController (yes i know i spelt delivery wrong)
also need to set the NSPredicate
this is home I'm preparing for segue in EmployeeTableViewController
these are my variables for EmployeeTableViewController
Setting the relationship
With one to many relationships, it is easiest to set the to-one relationship:
delivery.employee = employee
Put this line in your completeOrder method (you may need to pass the employee reference from a previous VC). CoreData will automatically set the inverse relationship for you, adding delivery to the deliveries set for the employee.
Showing the Deliveries in a Table View
Having set the relationship, the deliveries property of employee contains a set of Delivery objects to which it is related. To display these in a table view, create your deliveries array from this set (eg. in viewDidLoad):
deliveries = employee.delivery.allObjects()
Your numberOfRowsInSection can then just use deliveries.count and the cellForRowAtIndexPath can use deliveries[indexPath.row] to determine which Delivery to display in each cell.
(An alternative is to fetch the deliveries array in the normal way, but to use a predicate to restrict the results to those that are related to your employee:
fetch.predicate = NSPredicate(format:"employee == %#", employee)
Longer term, you should consider using NSFetchedResultsController which is designed for displaying CoreData objects in table view.)
Update
You don't need the thisEmployee variable. Just change the Employee variable to be Employee class:
var employee : Employee!
Then you should be able to set
deliveries = employee.deliveries?.allObjects as! [NSManagedObject]
And in your fetchDelivery() method, set the predicate with
fetchRequest.predicate = NSPredicate(format:"employee == %#", employee)
(after let fetchRequest = ....).
Update 2
It's difficult to see where the nil value is. To track it down, try printing the value of employee in the viewDidLoad method of the DelieveryTableViewController. If it's nil, there is a problem with passing the value in prepareForSegue. If not, print employee.deliveries, etc. Post your results.
give the structure:
Person
field1
...
fieldn
>>itemsTaken(inverse: takenFrom)
Item
field1
...
fieldn
>> takenFrom(inverse: itemsTaken)
Person.itemsTaken <<------>>Items.takenFrom
the scenario is that I have a list of Persons and a list of Items
now I would to show within the Person Detail View the items he taken (this is simply solved), and to show on the Item detail View the complete list of persons and select a subset of person that taken that item.
the problem is the 2nd view where I would to add/remove from the orderedset "takenFrom" a person.
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let mo = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject
if var x: NSMutableOrderedSet = mo.valueForKey("itemsTaken") as? NSMutableOrderedSet {
x.addObject(detailItem)
mo.setValue(x, forKey: "itemsTaken")
}
if var x: NSMutableOrderedSet = detailItem.valueForKey("takenFrom") as? NSMutableOrderedSet {
x.addObject(mo)
detailItem.setValue(x, forKey: "takenFrom")
}
(UIApplication.sharedApplication().delegate as AppDelegate).saveContext()
}
override func tableView(tableView: UITableView!, didDeselectRowAtIndexPath indexPath: NSIndexPath!) {
let mo = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject
if var x: NSMutableOrderedSet = mo.valueForKey("itemsTaken") as? NSMutableOrderedSet {
x.removeObject(detailItem)
mo.setValue(x, forKey: "itemsTaken")
}
if var x: NSMutableOrderedSet = detailItem.valueForKey("takenFrom") as? NSMutableOrderedSet {
x.removeObject(mo)
detailItem.setValue(x, forKey: "takenFrom")
}
(UIApplication.sharedApplication().delegate as AppDelegate).saveContext()
}
that works but when I restarted the app all references for relationship are lost
but I'm not sure I'm proceeding in the correct way.
before this approach I had 2 more entities in order to have for each entity a one 2 many relationships. I'm looking for a cleaner solution
do you have any suggestion or question?
just a clarification. the target is to add/remove references from person.itemsTaken and/or Item.takenFRom
I would to avoid to delete Person. can I remove only the reference within the relationship?
"Cleaner solution": first step would be to make your code more readable. Why call a variable mo or x if the object is better described as a person or as itemsTaken? An additional strategy to make your code more readable is to use NSManagedObject subclasses.
Second, it seems that you are adding the relationship twice (once for each side). I don't think this is necessary.
Third, you might want to check if your mutable ordered set is extracted the way you expect. I think it might be advisable to use the mutable accessor instead:
var itemsTaken = person.mutableSetValueForKey("itemsTaken")
Not sure if you still have to cast or do other things in order to keep the ordering in the ordered set. In my experience, the ordered set never really worked reliably even in Objective-C, so you might just want to keep an additional attribute for the ordering and change the model to use a simple many-to-many relationship.