CoreData many to many relationship Best practices - ios

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.

Related

Closure does not run in the main thread in swift

I want to show some images on UITableViewCell. However I got an error below
fatal error: Index out of range. The problem is that closure does not run in the main thread probably. How can I solve this issue?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
let program = urlToImageArray[indexPath.row] //index out of range
let urlToImage = NSURL(string: program)
cell.pickupImageView.sd_setImage(with: urlToImage as URL!)
return cell
}
Wrap anything you want to run on the main queue in DispatchQueue.main.async{ ... }.
That said, your current approach likely won't work. This method gets called a lot. While the user is scrolling, this method gets called every time a cell is about to come on the screen (in iOS 10, sometimes a bit before it'll come on the screen). Cells are often recycled, and you're appending data to the titleArray and other arrays every time a cell is requested (they may not be in order; they might have already been fetched; this array isn't going to wind up in the right order).
You need to move all your data about a cell into a model object and out of the view controller. There shouldn't be a titleArray and an urlArray, etc. There should just be an Article, and the Article should take care of fetching itself and updating its properties. And the job of this method is to fetch the correct Article from your cache, or create a new one if needed, and assign it to an ArticleCell. The ArticleCell should watch the Article and update itself any time the Article changes (i.e. when the fetch completes). Almost no work should happen directly in this method since it gets called so often, and in possibly random orders.
The common way to build this kind of thing is with a simple model object (often a reference type so it can be observed; there are many other approaches that allow a struct, but they're a little more advanced so we'll keep this simple):
class Article {
var author: String
var description: String
var publishedAt: Date
var title: String
var url: URL
var image: UIImage
func refresh() {
// fetch data from server and replace all the placeholder data
}
}
Then there's some kind of Model that vends these:
class Model {
func article(at index: Int) -> Article {
if let article = lookupArticleInCache(at: index) {
return article
}
let article = createAndCachePlaceholderArticle(at: index)
article.refresh()
}
}
And then your code looks like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
cell.article = sharedModel.article(at: indexPath.row)
return cell
}
You can use KVO or Swift Observables or an ArticleDelegate protocol to let the cell observe the Article. When the Article updates, the cell updates itself.
Again, there are many ways to approach this. You could have a "PlaceHolderArticle" that all the cells share and when the real Article comes in, the cell replaces the whole thing (so that Articles are immutable rather than self-updating). You could use the more generic approaches described by Swift Talk. There are lots of ways. But the key is that there is this model that updates itself, independent of any particular UI, and a UI (views, view controllers) that watch the model and display what it holds.
If you want much, much more on this topic, search for "Massive View Controller." That's the common name for the anti-pattern you're currently using. There are lots of ways to fight that problem, so don't assume that any particular article you read on it is "the right way" (people have come up with some very elaborate, and over-elaborate, solutions). But all of them are based on separating the model from the UI.
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
you have to make separate function for this calculation and try to avoid the any calculate functionality in "cellForRowAt"

Swift 3 - Core Data - Fetched Results Controller Access Relationship Attribute

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.

managedContext.insertObject extra argument in call `atIndex` in Swift?

So here is my code: (I want to re-order the table and update Core Data):
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
let entity = NSEntityDescription.entityForName(entity, inManagedObjectContext: managedContext)
let entityObject = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)
entityObject.setValue(content, forKey: key)
self.managedContext.insertObject(entityObject, atIndex: 0) //ERROR HERE
do {
try managedContext.save()
} catch let error as NSError {
}
}
I've seen similar code here but why mine isn't working? Thanks!
The link you provided is not doing the same thing you did here. What he did there was removing and inserting object in to a array of Playlist.
If you really want to re-order the table and update Core Data accordingly, you may want to add a index field to your Core Data model, and update it with the index of cell every time the cell is moved.
So you can populate the data to table view in order of the index filed, and keep cell order synchronized with data model.
First of all a side note: The objects in NSManagedObjectContext are unordered so there is no method to insert an object at an particular index.
Since the object is inserted already two lines above in the method NSManagedObject(entity:insertIntoManagedObjectContext:), delete the line which causes the error.
Agreed with Vadian, you do not need to worry about ordering the Managed Object Context.
To update the UI you should run -
"tableView.reloadData"
in addition to above code.

Rearranging order of TableView cells using Core Data

I have a custom UITableView class that handles animations for table view, and have tested the following code with an Array that is displayed in a table view.
tableView.didMoveCellFromIndexPathToIndexPathBlock = {(fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) -> Void in
self.objectsArray.exchangeObjectAtIndex(toIndexPath.row, withObjectAtIndex: fromIndexPath.row)
}
This works fine with a basic array, but I actually want to rearrange a managed object of a type NSSet. So in my file I have declared the following which creates returns an array of type Item which is used by the table view.
Folder class function:
func itemArray() -> [Item] {
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
return iist.sortedArrayUsingDescriptors([sortDescriptor]) as! [Item]
}
Table View Controller declaration
var itemArray = [Item]()
itemArray = folder.itemArray() //class function to return a NSArray of the NSSET [Item]
I am trying to make it so when rearranging the cells it changes the order of the NSSet so it is saved when the app reloads, does anyone have any suggestions ?
By definition, NSSets do not have an order so there is no native way for you to preserve the order of the NSSet. I am not saying it is not possible but you cannot do it the way you are thinking.
From Apple's NSSet Documentation:
The NSSet, NSMutableSet, and NSCountedSet classes declare the programmatic interface to an unordered collection of objects.
You will need to convert your NSSet to NSMutableArray and reference that array instead of the NSSet.

NSFetchedResultsController - Not calling back to delegate for inserts/deletes [duplicate]

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.

Resources