I've set up a model with a few string fields and a few array fields. The model saves as such:
- (void)saveLevel:(NSString*)level traps:(NSArray*)traps whirls:(NSArray*)whirls accels:(NSArray*)accels walls:(NSArray*)walls dest:(NSString*)dest jupiter:(NSString*)jupiter rating:(NSNumber*)pRating;
{
if (m_pMOC == nil)
// Retrieves the managed object context
NSManagedObject* pNewLevel = [NSEntityDescription insertNewObjectForEntityForName:#"Level" inManagedObjectContext:m_pMOC];
NSDate* pDate = [NSDate date];
// Must have these four attributes
[pNewLevel setValue:level forKey:#"Level_ID"];
[pNewLevel setValue:jupiter forKey:#"Ball"];
[pNewLevel setValue:pDate forKey:#"Creation_Date"];
[pNewLevel setValue:dest forKey:#"Destination"];
[pNewLevel setValue:pRating forKey:#"Rating"];
// Optional attributes
if ([traps count] != 0)
[pNewLevel setValue:traps forKey:#"Traps"];
if ([whirls count] != 0)
[pNewLevel setValue:whirls forKey:#"Whirls"];
if ([accels count] != 0)
[pNewLevel setValue:accels forKey:#"Accelerators"];
if ([walls count] != 0)
[pNewLevel setValue:walls forKey:#"Walls"];
NSError* pError;
if (![m_pMOC save: &pError])
// etc...
}
I have a DataManager class which handles the fetching/saving from/to Core Data, and inside of the Manager I've verified that when I fetch an entity, the arrays are fetched, but when it's returned to the class that's calling it, the arrays are nil (the strings, on the other hand, arrive just fine). Here's the fetch code and the code that parses out the returned value from the fetch:
- (NSArray*) getLevelWithID:(NSString*)level
{
NSEntityDescription *pEntityDescription = [NSEntityDescription entityForName:#"Level" inManagedObjectContext:m_pMOC];
NSFetchRequest *pRequest = [[[NSFetchRequest alloc] init] autorelease];
[pRequest setEntity:pEntityDescription];
NSPredicate* pPredicate = [NSPredicate predicateWithFormat:#"Level_ID == %#", level];
[pRequest setPredicate: pPredicate];
NSError* pError;
NSArray* pLevels = [m_pMOC executeFetchRequest:pRequest error:&pError];
if (pLevels == nil)
{
NSLog(#"Could not find the level with Level_ID = %#",level);
abort();
}
NSArray* pAccels = [pLevels valueForKey:#"Accelerators"];
NSArray* pTraps = [pLevels valueForKey:#"Traps"];
NSArray* pWalls = [pLevels valueForKey:#"Walls"];
NSArray* pWhirls = [pLevels valueForKey:#"Whirls"];
return pLevels;
}
I set a break point on the last four Arrays, and they have objects in them, but in the function that retrieves them (shown below), they are nil.
- (void) initLevel:(NSArray*)pLevelObjects
{
Level* pLevel = [pLevelObjects objectAtIndex:0];
NSString* pJupiter = pLevel.Ball;
NSString* pDest = pLevel.Destination;
NSArray* pAccels = [pLevel valueForKey:#"Accelerators"];
NSArray* pTraps = [pLevel valueForKey:#"Traps"];
NSArray* pWalls = [pLevel valueForKey:#"Walls"];
NSArray* pWhirls = [pLevel valueForKey:#"Whirls"];
... (other initialization of the level) ...
}
I'm perplexed by this. The string values are there, but the arrays are not. I tried using the dot notation originally (NSArray* pAccels = pLevel.Accelerators, etc), but with the same result.
Ideas?
EDIT: initLevel is called from the view controller:
- (void) startGameWithLevelID:(NSString*)pLevelID
{
NSLog(#"MyViewController - beginGameWithLevelID");
NSArray* pLevel = [[DataManager getDataManager] getLevelWithID:pLevelID];
if (pLevel == nil || [pLevel count] == 0)
{
NSLog(#"Didn't retrieve any level with id %#", pLevelID);
abort();
}
else
{
CGRect newFrame = makeScreen([UIScreen mainScreen].applicationFrame);
GameBoard* pGB = [[GameBoard alloc] initWithFrame: newFrame];
pGB.m_pMyVC = self;
[pGB initLevel: pLevel];
[self.view addSubview: (UIView*)pGB];
[pGB release];
}
}
EDIT: I've newly re-factored the arrays into separate entities with a to-one relationship with the Level; the level has a to-many relationship with these entities. I also changed the names to better adhere to conventions: all attributes and relationships now start with a lowercased letter. I've prefixed the attributes and relationships with a_ and r_ respectively. I'm getting an error when saving "-[NSCFString _isKindOfEntity:]: unrecognized selector sent to instance 0x4d83120". It isn't terribly helpful but I'll let you know when I find the issue. For now, the code for saving looks as such:
- (void)saveLevel:(NSString*)level traps:(NSArray*)traps whirls:(NSArray*)whirls accels:(NSArray*)accels walls:(NSArray*)walls dest:(NSString*)dest jupiter:(NSString*)jupiter rating:(NSNumber*)pRating;
{
if (m_pMOC == nil)
{ // Code to get the managed object context from the delegate
}
NSManagedObject* pNewLevel = [NSEntityDescription insertNewObjectForEntityForName:#"Level" inManagedObjectContext:m_pMOC];
NSManagedObject* pNewBall = [NSEntityDescription insertNewObjectForEntityForName:#"Ball" inManagedObjectContext:m_pMOC];
[pNewBall setValue:jupiter forKey:#"a_Bounds"];
NSManagedObject* pNewDest = [NSEntityDescription insertNewObjectForEntityForName:#"Dest" inManagedObjectContext:m_pMOC];
[pNewDest setValue:dest forKey:#"a_Bounds"];
NSDate* pDate = [NSDate date];
// Must have these four attributes
[pNewLevel setValue:level forKey:#"a_Level_ID"];
[pNewLevel setValue:pDate forKey:#"a_Creation_Date"];
[pNewLevel setValue:pRating forKey:#"a_Rating"];
[pNewLevel setValue:#"Bob Dole" forKey:#"a_CreatedBy"];
[pNewLevel setValue:pNewBall forKey:#"r_Ball"];
[pNewLevel setValue:pNewDest forKey:#"r_Dest"];
// Optional attributes
if ([traps count] != 0)
[[pNewLevel mutableSetValueForKey: #"r_Trap"] addObjectsFromArray: traps];
if ([whirls count] != 0)
[[pNewLevel mutableSetValueForKey: #"r_Whirl"] addObjectsFromArray: whirls];
if ([accels count] != 0)
[[pNewLevel mutableSetValueForKey: #"r_Accel"] addObjectsFromArray: accels];
if ([walls count] != 0)
[[pNewLevel mutableSetValueForKey: #"r_Wall"] addObjectsFromArray: walls];
NSError* pError;
if (![m_pMOC save: &pError])
{ // Error saving
}
else
{ // Successfully saved
}
}
Again, thanks for the help, sorry for the lengthy post but I'm hoping this will help other Core Data newbies (like me) learn something.
FINAL EDIT: Ok, so after refactoring, I finally found a solution to save the arrays as objects (though whether it's the most efficient way I'm not sure). Below is just the code for adding a bunch of "Traps". The array passed into the function is the bounds of the Trap, which will be stored. The relationship is then formed with the Level (note it doesn't have to be set by both the Trap and the Level, just one of them, since Core Data takes care of the other way).
if ([traps count] != 0)
{
for (NSString* pBounds in traps)
{
NSManagedObject* pNewTrap = [NSEntityDescription insertNewObjectForEntityForName:#"Trap" inManagedObjectContext:m_pMOC];
[pNewTrap setValue:pBounds forKey:#"a_Bounds"];
[pNewTrap setValue:pNewLevel forKey:#"r_Level"];
}
}
Possible Array Misuse
In your first snippet, you are calling valueForKey: on an NSArray, and the result is an array, because that code iterates over pLevels, passes your valueForKey: argument to each object inside and combines the values.
It is how NSArray implements Key-Value Coding.
In your second piece of code, where you first fetch one pLevel from the array, you perform your valueForKey: selector on an entity, not on an array. So the result is what's in that particular object.
Probably some of your entities have those fields, so a combined result from all of those has some values, but some particular ones do not.
Possible Core Data Misuse
You say that you keep an array in your Core Data entity. How do you do that? There isn't a special type to hold an array, so normally it has to be stored as binary data with performing encoding using [NSKeyedArchiver archivedDataWithRootObject:array]. It might be that your storing and fetching is incorrect because you're trying to store arrays in non-array fields.
Do you also experience any crashes?
Related
I have an array of NSDictionary. Each array item has a key named "Name". Now I want to remove duplicate entries based on this name value.
This work perfectly:
aMyArray = [aMyArray valueForKeyPath:#"#distinctUnionOfObjects.Name"];
The problem are that result array still contains duplicates string, based on the case. Ex: [#"Franck", "franck"]
How can I remove these duplicate?
Thanks
You could try to do this
// in your class implementation
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[self class]]) {
return NO;
}
typeof(self) obj = (typeof(self))object;
return ([self.Name caseInsensitiveCompare:obj.Name] == NSOrderedSame);
}
- (NSUInteger)hash
{
return [[self.Name lowercaseString] hash];
}
// and then
NSSet *distinctObjects = [[NSSet alloc] initWithArray:array];
NSArray *result = distinctObjects.allObjects;
Alternatively you could customise this KVC collection operator by swizzling valueForKeyPath: to parse your custom DSL, possibly winding up with something like
aMyArray = [aMyArray valueForKeyPath:#"#distinctUnionOfObjects[caseInsensitive].Name"];
which doesn't seem to be a good idea for me, but it certainly a viable solution to your problem.
I have a complex CoreData entity: MY_ENTITY
I receive a Object of type MY_ENTITY from my webService.
In some cases, I need to edit my local CoreData obj (MY_ENTITY) with received obj.
So:
I have OBJ_1 in CoreData
I receive OBJ_2 from WebService.
I need to update OBJ_1 from OBJ_2.
Have I to set all field or can I assign OBJ_1 ObjectID to OBJ_2 and save the context (same Context)?
Since they are two separate instances, you need to move what you want from O2 to O1. You can use a routine such as this to do the move attribute by attribute assuming both objects are of the same Entity class:
// use entity description to get entity attributes and use as keys to get value
// scan attributes
NSDictionary *attributes = [[sourceEntity entity] attributesByName];
for (NSString *attribute in attributes) {
id value = [sourceEntity objectForKey:attribute];
if (value == nil) {
continue;
}
NSAttributeType attributeType = [[attributes objectForKey:attribute] attributeType];
switch (attributeType) {
case NSStringAttributeType:
// value = [value stringValue];
break;
case NSInteger16AttributeType:
case NSInteger32AttributeType:
case NSInteger64AttributeType:
case NSBooleanAttributeType:
value = [NSNumber numberWithInteger:[value integerValue]];
break;
case NSFloatAttributeType:
case NSDecimalAttributeType:
value = [NSNumber numberWithDouble:[value doubleValue]];
break;
case NSDateAttributeType:
if (dateFormatter != nil)
value = [dateFormatter stringFromDate:value];
break;
default:
value = #"";
break;
}
[targetEntity setValue:value forKey:attribute];
}
Note, this is just an example and would need to be cleaned up and have error handling added if you intend to use. Also, if you are getting O2 via a webservice as JSON or XML, then you can use this to simply push the JSON payload into the targetEntity. This assumes your payload attrs align with your entity attrs. In this case you would replace sourceEntity with the JSON payload using a block or equivalent for example:
NSArray *seedData = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:dataPath]
options:kNilOptions
error:&err];
[seedData enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
...
id value = [obj objectForKey:attribute];
...
}
In which format are you receiving OBJ_2 from the Web Service?
Either way, assigning OBJ_2 to OBJ_1 would not work since you would only replace the reference your local variable is pointing to.
To synchronize your local CoreData Entity which the data from the server you will need to modify the attributes of your existing entitiy. Depending on your data model and the format you are receiving OBJ_2 in there are different ways to achieve this.
I have two UISlider's that represent minimum and maximum prices of items. I am passing these as well as other various data back to the previous controller.
I've used a protocol method and set the previous controller as a delegate to make it possible to pass values back to the controller.
I can easily grab the other objects out of the array because they're strings. I just use:
[_finalSelectionForRefinement containsObject:#"size"];
This is what I do in pushed controller:
// create dictionary with keys minimum and maximum that hold the
// position of the slider as a float
_dictionaryWithSliderValues =
[[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithFloat:_minSliderPosition], #"minimum",
[NSNumber numberWithFloat:_maxSliderPosition], #"maximum", nil];
// store this in the array that is retrieved in previous controller
[_finalSelectionForRefinement addObject:_dictionaryWithSliderValues];
My question is how do I now use the minimum and maximum keys to grab the slider position float objects?
Thought I could use NSPredicate but the examples I've been coming across on blogs as well as youTube are of no help to my specific needs.
Would appreciate some help here
Regards
UPDATE - Short snipped of method in previous controller where I need to retrieve the slider minimum and maximum values:
-(PFQuery *)queryForCollection
{
PFQuery *query = [PFQuery queryWithClassName:#"Garments"];
if (_selectedRowInFilterPicker == 0) {
NSLog(#"ORDER BY RECOMMENDED");
[query orderByDescending:#"recommended"];
} else if (_selectedRowInFilterPicker == 1) {
NSLog(#"ORDER BY NEWEST ITEMS");
[query orderByDescending:#"createdAt"];
} else if (_selectedRowInFilterPicker == 2) {
NSLog(#"ORDER BY DESCENDING USING PRICE");
[query orderByDescending:#"price"];
} else if (_selectedRowInFilterPicker == 3) {
NSLog(#"ORDER BY ASCENDING USING PRICE");
[query orderByAscending:#"price"];
}
if ([_selectionFromRefineResultsController count] > 0) {
NSLog(#"Selection FROM REF MADE");
// Gender
if ([_selectionFromRefineResultsController containsObject:#"Male"]) {
[query whereKey:#"gender" equalTo:#1];
}
if ([_selectionFromRefineResultsController containsObject:#"Female"]) {
[query whereKey:#"gender" equalTo:#2];
}
}
// Here I need to check there is a minimum or maximum value in the array
// If there is I can user [query whereKey:#"price" greaterThan:MINVAL] and MAXVAL
// This will return items within the correct price range.
This queryForCollection method is called from within another method called performQuery which is called when the button of the second controller is tapped to pass data back to the controller that pushed it in the first place.
You should look at the documentation for NSMutableArray , NSArray and NSDictionary
Which will explain the instance methods for each.
But in a nutshell any object that you add should be in a NSDictionary so it has a value and a key. This includes any of your strings. Doing so simplifies how you search by using keys.
If the NSMutableArray contains objects that are not KVC then you will I think find it harder it go through the objects in one sweep.
Because NSmutableArray inherites from NSArray you can then use instance method valueForKey: on a NSmutableArray whose objects values or objects objects values have keys.
valueForKey:
Returns an array containing the results of invoking
valueForKey: using key on each of the array's objects.
(id)valueForKey:(NSString *)key
Rough Example:
NSMutableArray * finalSelectionForRefinement =[[NSMutableArray alloc]init];
NSDictionary *dictionaryWithSliderValues = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithFloat:10], #"minimum", [NSNumber numberWithFloat:20], #"maximum", nil];
NSDictionary *stringValues = [[NSDictionary alloc] initWithObjectsAndKeys:#"the-size", #"size", #"the-hight", #"hight", nil];
[finalSelectionForRefinement addObject:dictionaryWithSliderValues];
[finalSelectionForRefinement addObject:stringValues];
NSLog(#"finalSelectionForRefinement = %#", [finalSelectionForRefinement valueForKey:#"maximum"] );
First off, you can of store everything in one NSDictionary which makes more sense. But I wanted to show you that the valueForKey: will search within each.
The other thing is valueForKey: will return an NSArray containing the results. any objects that it finds that do not match the key you are looking for will be returned as an NSNull object. i.e
finalSelectionForRefinement = (
20,
"<null>"
)
So you would need to still single your value out. One way is use a objectEnumerator like this:
NSEnumerator *enumerator = [[finalSelectionForRefinement valueForKey:#"maximum"] objectEnumerator];
id anObject;
while (anObject = [enumerator nextObject]) {
if(![anObject isKindOfClass:[NSNull class]])
{
NSLog(#"anObject = %#", anObject);
}
}
Which should return:
anObject = 20
There are most likely better ways of doing this. All of the above is just to give you one idea. And I suspect you could cut out a lot of the code by using bindings.
(also note this answer was being constructed before you question update)
What is the simplest way to do a binary search on an (already) sorted NSArray?
Some potential ways I have spotted so far include:
The use of CFArrayBSearchValues (mentioned here) - would this work on an NSArray?
The method indexOfObject:inSortedRange:options:usingComparator: of NSArray assumes the array is sorted and takes an opts param of type NSBinarySearchingOptions - does this mean it performs a binary search? The docs just say:
Returns the index, within a specified range, of an object compared with elements in the array using a given NSComparator block.
Write my own binary search method (something along the lines of this).
I should add that I am programming for iOS 4.3+
Thanks in advance.
The second option is definitely the simplest. Ole Begemann has a blog entry on how to use the NSArray's indexOfObject:inSortedRange:options:usingComparator: method:
NSArray *sortedArray = ... // must be sorted
id searchObject = ...
NSRange searchRange = NSMakeRange(0, [sortedArray count]);
NSUInteger findIndex = [sortedArray indexOfObject:searchObject
inSortedRange:searchRange
options:NSBinarySearchingFirstEqual
usingComparator:^(id obj1, id obj2)
{
return [obj1 compare:obj2];
}];
See NSArray Binary Search
1 and 2 will both work. #2 is probably easier; it certainly doesn't make sense for that method to do anything other than a binary search (if the range is above a certain size, say). You could verify on a large array that it only does a small number of comparisons.
I'm surprised that nobody mentioned the use of NSSet, which [when it contains objects with a decent hash, such as most Foundation data types] performs constant time lookups. Instead of adding your objects to an array, add then to a set instead (or add them to both if you need to retain a sorted order for other purposes [or alternatively on iOS 5.0 or Mac OS X 10.7 there is NSOrderedSet]).
To determine whether an object exists in a set:
NSSet *mySet = [NSSet setWithArray:myArray]; // try to do this step only once
if ([mySet containsObject:someObject])
{
// do something
}
Alternatively:
NSSet *mySet = [NSSet setWithArray:myArray]; // try and do this step only once
id obj = [mySet member:someObject];
// obj is now set to nil if the object doesn't exist or it is
// set to an object that "isEqual:" to someObject (which could be
// someObject itself).
It is important to know that you will lose any performance benefit if you convert the array to a set each time you do a lookup, ideally you will be using a preconstructed set containing the objects you want to test.
//Method to pass array and number we are searching for.
- (void)binarySearch:(NSArray *)array numberToEnter:(NSNumber *)key{
NSUInteger minIndex = 0;
NSUInteger maxIndex = array.count-1;
NSUInteger midIndex = array.count/2;
NSNumber *minIndexValue = array[minIndex];
NSNumber *midIndexValue = array[midIndex];
NSNumber *maxIndexValue = array[maxIndex];
//Check to make sure array is within bounds
if (key > maxIndexValue || key < minIndexValue) {
NSLog(#"Key is not within Range");
return;
}
NSLog(#"Mid indexValue is %#", midIndexValue);
//If key is less than the middleIndexValue then sliceUpArray and recursively call method again
if (key < midIndexValue){
NSArray *slicedArray = [array subarrayWithRange:NSMakeRange(minIndex, array.count/2)];
NSLog(#"Sliced array is %#", slicedArray);
[self binarySearch:slicedArray numberToEnter:key];
//If key is greater than the middleIndexValue then sliceUpArray and recursively call method again
} else if (key > midIndexValue) {
NSArray *slicedArray = [array subarrayWithRange:NSMakeRange(midIndex+1, array.count/2)];
NSLog(#"Sliced array is %#", slicedArray);
[self binarySearch:slicedArray numberToEnter:key];
} else {
//Else number was found
NSLog(#"Number found");
}
}
//Call Method
#interface ViewController ()
#property(nonatomic)NSArray *searchArray;
#end
- (void)viewDidLoad {
[super viewDidLoad];
//Initialize the array with 10 values
self.searchArray = #[#1,#2,#3,#4,#5,#6,#7,#8,#9,#10];
//Call Method and search for any number
[self binarySearch:self.searchArray numberToEnter:#5];
// Do any additional setup after loading the view, typically from a nib.
}
CFArrayBSearchValues should workâNSArray * is toll-free bridged with CFArrayRef.
I have array made from JSON response.
NSLog(#"%#", arrayFromString) gives the following:
{
meta = {
code = 200;
};
response = {
groups = (
{
items = (
{
categories = (
{
icon =
"http://foursquare.com/img/categories/parks_outdoors/default.png";
id = 4bf58dd8d48988d163941735;
and so on...
This code
NSArray *arr = [NSArray arrayWithObject:[arrayFromString valueForKeyPath:#"response.groups.items"]];
gives array with just one element that I cannot iterate through. But if I write it out using NSLog I can see all elements of it.
At the end I would like to have an array of items that I can iterate through to build a datasource for table view for my iPhone app.
How would I accomplish this?
EDIT:
I have resolved my issue by getting values from the nested array (objectAtIndex:0):
for(NSDictionary *ar in [[arrayFromString valueForKeyPath:#"response.groups.items"] objectAtIndex:0]) {
NSLog(#"Array: %#", [ar objectForKey:#"name"]);
}
First, the data structure you get back from the JSON parser is not an array but a dictionary: { key = value; ... } (curly braces).
Second, if you want to access a nested structure like the items, you need to use NSObject's valueForKeyPath: method. This will return an array of all items in your data structure:
NSLog(#"items: %#", [arrayFromString valueForKeyPath:#"response.groups.items"]);
Note that you will loose the notion of groups when retrieving the item objects like this.
Looking at the JSON string you posted, response.groups.items looks to be an array containing one item, a map/dictionary containing one key, "categories." Logging it out to a string is going to traverse the whole tree, but to access it programmatically, you have to walk the tree yourself. Without seeing a more complete example of the JSON, it's hard to say exactly what the right thing to do is here.
EDIT:
Traversing an object graph like this is not that simple; there are multiple different approaches (depth-first, breadth-first, etc,) so it's not necessarily something for which there's going to be a simple API for you to use. I'm not sure if this is the same JSON library that you're using, but, for instance, this is the code from a JSON library that does the work of generating the string that you're seeing. As you can see, it's a bit involved -- certainly not a one-liner or anything.
You could try this, which I present without testing or warranty:
void __Traverse(id object, NSUInteger depth)
{
NSMutableString* indent = [NSMutableString string];
for (NSUInteger i = 0; i < depth; i++) [indent appendString: #"\t"];
id nextObject = nil;
if ([object isKindOfClass: [NSDictionary class]])
{
NSLog(#"%#Dictionary {", indent);
NSEnumerator* keys = [(NSDictionary*)object keyEnumerator];
while (nextObject = [keys nextObject])
{
NSLog(#"%#\tKey: %# Value: ", indent, nextObject);
__Traverse([(NSDictionary*)object objectForKey: nextObject], depth+1);
}
NSLog(#"%#}", indent);
}
else if ([object isKindOfClass: [NSArray class]])
{
NSEnumerator* objects = [(NSArray*)object objectEnumerator];
NSLog(#"%#Array (", indent);
while (nextObject = [objects nextObject])
{
__Traverse(nextObject, depth+1);
}
NSLog(#"%#)", indent);
}
else
{
NSLog(#"%#%#",indent, object);
}
}
void Traverse(id object)
{
__Traverse(object, 0);
}