I have an array of segments each has an array of fighters associated with it.
The relationship can be described thusly:
One `segment` can have many `fighters`
The objects are managed by core data.
When displayed in a table view each cell has two buttons so a user can pick a fighter from a seperate view controller; and it then when it returns it will update the segment fighter relationship.
Because the relationship of segment.fighters a non-mutable NSSet, I need to modify it so that;
If
User presses left button, this infers that the first object in the segment.fighter is being picked
User presses right button, it infers that the last object in the segment.fighter is being picked
When I come to Update the relationship, sometimes; depending on the sequence of buttons pressed by the user; the positions of the objects in the NSSet are inverted.
ie:
Sometimes a fighter that should be at position 0 is swapped with a
fighter at position 1, when this should never happen.
My code is as follows;
NSArray *currentFighters = [[segment valueForKeyPath:#"fighters"] allObjects];
NSLog(#"----- current fighters ---- ");
for (FCFighter *fighter in currentFighters) {
NSLog(#"%#", [fighter description]);
}
NSMutableArray *currentFightersMutable = [currentFighters mutableCopy];
[currentFightersMutable replaceObjectAtIndex:fighterIdx withObject:pickedFighter];
NSArray *updatedFighters = [currentFightersMutable copy];
NSLog(#"----- updated fighters ---- ");
for (FCFighter *fighter in updatedFighters) {
NSLog(#"%#", [fighter description]);
}
[segment setFighters:[NSSet setWithArray:updatedFighters]];
NSLog(#"----- [segment fighters] ---- ");
for (FCFighter *fighter in [[segment valueForKeyPath:#"fighters"] allObjects]) {
NSLog(#"%#", [fighter description]);
}
I am using a NSMutableArray so that I can replace the exact object in the array.
I can prove that it works via logs
Picked fighter - AJ Fonseca
----- current fighters ----
AJ Matthews
A Sol Kwon
----- updated fighters ----
AJ Fonseca
A Sol Kwon
----- [segment fighters] ---- // NOTE it has swapped them, but why?
A Sol Kwon
AJ Fonseca
-[FCSegmentTableViewCell configureCellWithSegment:]
#0 -- A Sol Kwon
#1 -- AJ Fonseca
However when it gets to segment fighters output, the fighters are swapped; but my question is --
Why does the NSSet invert my array?
Many thanks
An NSSet is an unordered collection of objects. The property allObjects which you use returns an array, but as the documentation for this method states "The order of the objects in the array is undefined.".
Because of this, whenever you try and get an array out of your set, you could get a different order.
To make sure the orders don't change, you should either a) use an ordered relationship for the fighters - but not a good idea as NSOrderedSet is a mix between an array and a set. b) apply a sort to the array returned from allObjects so that the order is consistent. This is my preferred method.
Related
Consider a Core Data model with two Entities: TermDictionary and Term. The TermDictionary has a "name" property, and a one-to-many relationship called "terms" which points to a set of Term objects, each of which consists of two properties: "name" and "score".
I've got an NSFetchRequest which I'm using as a data source for a UITableView which displays all of the TermDictionaries in the database. The idea is that the table will, for each cell, display the name of the dictionary, along with a count of the number of terms in that dictionary.
In the following code snippet, item contains an NSFetchRequestResult for the "TermDictionary" entity:
let thisDict = item as! TermDictionary
cell.textLabel?.text = thisDict.name
cell.detailTextLabel?.text = "\(thisDict.terms?.count ?? 0) terms"
...The table cells are correctly displaying the names of the Term Dictionaries, however it looks like thisDict.terms is always coming up nil, so the number-of-terms label always shows zero.
Do I need do do something special with item rather than just casting it to my TermDictionary managed object subclass?
You do not need to do anything special. If thisDict.terms prints as nil, it really is nil. Check your data store.
I am using UIManagedDocument with Parent Child context.
In my child context I do the following
Code 1
NSSet *results = [self.event.memberships filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return ([[evaluatedObject deleted] boolValue] == NO);
}]];
Above code returns the expected results (only Not deleted members for the event).
Code 2
But this code does not. It fetches all records.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"deleted == NO"];
NSSet *results = [self.event.memberships filteredSetUsingPredicate:predicate];
It seems confusing. Both should return same results, but predicateWithBlock returns correct results where as predicateWithFormat returns all records.
What are the pros and cons of using predicateWithBlock instead of predicateWithFormat?
The problem is that you have defined an attribute deleted for your entity. That conflicts with the isDeleted method of NSManagedObject, so you should rename that attribute.
The following "experiment" shows that strange things happen if you call your attribute "deleted" (c is a managed object with a custom deleted attribute):
// Set custom "deleted" property to YES:
c.deleted = #YES;
// Use the property, as your Code 1
NSLog(#"%#", [c deleted]);
// Output: 1
// Use Key-Value Coding, as your Code 2
NSLog(#"%#", [c valueForKey:#"deleted"]);
// Output: 0
// Now really delete the object and try again:
[context deleteObject:c];
NSLog(#"%#", [c valueForKey:#"deleted"]);
// Output: 1
Your "Code 1" refers to the property, therefore it returns the expected result. "Code 2" uses Key-Value Coding, and [c valueForKey:#"deleted"] returns YES if the object
actually has been deleted from the context!
So renaming that attribute should solve your problem. Unfortunately the compiler does not
emit warnings if an attribute name conflicts with a built-in method.
Use the formatting placeholder to replace the bool value:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K == %#",
#"deleted", #(NO)];
Your use of the key path is probably ok, but the right-hand side probably doesn't look like "NO" to the parser.
SCENARIO
I have two entities: Item and ListDetail (which contains prices for different lists for every item). This is absolutely needed and I can't provide a price attribute for the Item entity because every item can have more prices for different dynamic lists (retail, b2b ecc.).
The relationship is:
Item (lists) <------->> (item) ListDetail
The current active list in my app change dinamically, so let's say I have an integer variable with the current active list: _ACTIVE_LIST_CODE_. When I need a price for an item object I use an helper method on the Item class:
-(NSNumber*) getPrice {
NSSet *lists=[self.lists filteredSetUsingPredicate: [NSPredicate predicateWithFormat:#"listId == %d",_ACTIVE_LIST_CODE_]];
ListDetail *activeList=[[lists allObjects] objectAtIndex:0];
return activeList.price;
}
THE PROBLEM
I use a UITableView with NSFetchedResultController in order to select and show some items for different sections. Nothing special. I would like to order the fetchedObjects using the items price for the active list. If price was an attribute of Item I would added simply a sort descriptor to the fetch request like so:
[NSSortDescriptor sortDescriptorWithKey:#"price" ascending:YES];
But as said before this is not possible, price is a dynamic attribute.
If using transient properties was possible for sort descriptors, I would set a price transient properties calculated on fly using my helper method. Nothing to do.
Using a keypath in the descriptor like "lists.price" is not possible (or maybe I don't know how to do that), just because it's a to-many relationship and it's modeled with a NSSet.
I tried some workaround, without success:
1) observing _ACTIVE_LIST_CODE_ changes to set items price in a non-transient attribute.
2) after the fetch request, before presenting the table view, reorder a brand new array with fetched objects using the transient "price" property, iterate the orderdered array following an ascending integer index "i" and assigning this value to a non-transient property "order" for the Item entity. Using "order" for sort descriptor in the fetch request. (This approach is described here: Re-ordering NSFetchedResultsController)
Both of them works, but they slow down performance because I have thousands of items in the fetch results... Any idea?
How about fetching ListDetail instead? You could restrict and sort with the appropriate predicates and sort descriptors, exactly as you propose.
fetchRequest.predicate =
[NSPredicate predicateWithFormat:#"listID = %#", activeListCode];
fetchRequest.sortDescriptors =
#[[NSSortDescriptor sortDescriptorWithKey:#"price" ascending:YES]];
Now, to group by some attribute of item should be simple and efficient because it is a to-one relationship. Your fetched results controller's sectionNameKeyPath can be something like
#"item.category"
As per the title really. I have an entity which has a property "idNumber". Just as I can bind a text box to the array controller's arrangedObjects with Model Key Path "#count" to provide a count of all the objects in the array, I would like to be able to bind a text field to the array controller's arrangedObjects with a value transformer to return a count of a filtered subset of the array (those objects with an idNumber >5).
I'm assuming this is possible??
My attempt is:
I have bound the text box to the array controller, Controller Key "arrangedObjects" Model Key Path "" Value Transformer "AllToSomeTransformer".
The code for the AllToSomeTransformer is:
-(id)transformedValue:(id)value {
NSArray *arrayOfAllCars;
if (value == nil) return nil;
if ([value respondsToSelector: #selector(count)]) {
arrayOfAllCars = [NSArray arrayWithArray:value];
} else {
[NSException raise: NSInternalInconsistencyException
format: #"Value (%#) does not respond to -count.",
[value class]];
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"idNumber > %#", [NSNumber numberWithInt:5]];
NSArray *arrayOfBlueCars = [arrayOfAllCars filteredArrayUsingPredicate:predicate];
return [NSNumber numberWithInt:[arrayOfBlueCars count]];
}
I believe my value transformer is correctly registered etc. By way of trying to figure out what's going on I added some NSLog outputs through to above code. It appears the above method is only called once, on app startup, and not again when new objects are added to the array. Could this be why the text field is not being updated with values??
Thanks, Oli
Since the transformer is called and does work but only once, that suggest there is something wrong with the bindings such that the transformer is not observing the changes in arrangedObjects. I'm not sure what that would be.
say i have NSManagedObject A, that has a many-to-many relationship to NSManagedObject B.
I have one saved instance of A and B. (not yet related)
Now I want A to save the instance of B twice in its relationship, which is of course a set.
Though, since its a set it stores only one reference of B.
see: (not syntax checked)
NSArray *tmpArray = [NSArray arrayWithObjects: B1, B1, nil];
[A setB: [NSSet setWithArray: tmpArray]];
-> only one B is stored in that relationship..
Is it possible to keep track of both B's ?
You can't do this. Core Data is not maintaining an array but an object-graph.
An object-graph store the relationships between objects. Since each object is unique, it makes no sense to have a duplicate relationship because that conveys no information. Suppose you have an object Person instance Jane that has a brothers relationship which contains three objects, Steve,John and Mike. It would be logically nonsensical to have two relationships to Steve because the real Steve that the object models isn't Jane's brother twice. Even if Jane did have two brothers named Steve, they would still be seperate individuals requiring their own objects to represent them in the object-graph.
If you find yourself thinking you require duplicate relationships, then you've probably misunderstood something about how the object-graph works.
I had a similar issue and searched but could not find anything. Indeed I was thinking about the relationship incorrectly.
The app is arranging a bunch of Items on a Board in sequence with repeats.
EX. Do A, Do B, Do C, Do A again, Do D
I created a good old-fashioned join table with the Item, Board and board position called BoardItemPositon.
From the Board entity you can hide all of the join table messiness and get and set an array.
#implementation Board
#dynamic boardItems;
- (void)setItems:(NSArray *)items{
//the ordered set of links
NSMutableOrderedSet *boardItemSet = [NSMutableOrderedSet new];
int i = 1;
for (Item *item in items) {
BoardItemPosition *boardItemPosition = (BoardItemPosition *)[NSEntityDescription insertNewObjectForEntityForName:#"BoardItemPosition"
inManagedObjectContext:[self managedObjectContext]];
NSNumber *num = [NSNumber numberWithInt:i++];
[boardItemPosition setItemPosition:num];
[boardItemPosition setItem:item];
[boardItemSet addObject:boardItemPosition];
}
//delete the old links
for (BoardItemPosition *boardItemPosition in [self boardItems]) {
[self.managedObjectContext deleteObject:[self.managedObjectContext objectWithID:boardItemPosition.objectID]];
}
//set the new links
[self setBoardItems:boardItemSet];
}
- (NSArray *)items{
//pull out item and add to array
NSMutableArray *itemArray = [NSMutableArray new];
for (BoardItemPosition *boardItemPosition in [self boardItems]) {
[itemArray addObject:[boardItemPosition item]];
}
return itemArray;
}