I have an method that reads an xml file and stores the xml nodes at a certain XPath-path in an NSArray called *nodes. What I want to do is take each one of the items in the array and add it to a core data entity called Category with the attribute of "name".
I have tried a number of different ways of creating the entity but I'm not sure about the correct way to do this effectively. This is the code used to create the NSArray, any ideas on how to implement this? (ignore the NSError, I will fix this in the final version)
- (IBAction)readCategories:(id)sender
{
NSString *xmlString = [resultView string];
NSData *xmlData = [xmlString dataUsingEncoding: NSASCIIStringEncoding];
NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithData:xmlData options:nil error:nil];
//XPath
NSError *err=nil;
NSArray *nodes = [xmlDoc nodesForXPath:#"//member[name='description']/value/string" error:&err];
}
EDIT - My loop code
NSArray *nodes = [xmlDoc nodesForXPath:#"//member[name='description']/value/string" error:&err];
int arrayCount = [nodes count];
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSXMLElement *categoryEl;
NSString *new = [catArrayController newObject];
int i;
for (i = 0; i < arrayCount; i++)
{
[categoryEl = [nodes objectAtIndex:i]];
[new setValue:[categoryEl stringValue] forKey:#"name"];
[catArrayController addObject:new];
}
[pool release];
Here's how I'd write it:
for (NSXMLElement *categoryElement in nodes) {
NSManagedObject *newObject = [catArrayController newObject];
[newObject setValue:[categoryElement stringValue] forKey:#"name"];
[catArrayController addObject:newObject];
[newObject release];
}
First, I'm using the Objective-C 2.0 for-each syntax. This is simpler than using index variables. I eliminated i and arrayCount.
Next, I took out your NSAutoreleasePool. None of the objects in the loop are autoreleased, so it had no effect. (The newObject method returns a retained object which is, by convention, what methods with the word new in their name do) This is also why I release newObject after adding it to the array controller. Since I'm not going to be using it any more in this method, I need to release it.
Also, you had defined new (which I renamed newObject) as an NSString. Core Data objects are always either an instance of NSManagedObject or a subclass of NSManagedObject.
Your line [categoryEl = [nodes objectAtIndex:i]] won't compile. That's because the bracket syntax is used to send a message to an object. This is an assignment statement, so the bracket syntax is not needed here. (This line is also not necessary any more because of I've changed the loop to use the for-each syntax) But, for future reference, categoryEl = [nodes objectAtIndex:i]; would have worked.
What part are you having trouble with? There shouldn't be much more to it than looping through the array, creating a new managed object for each entry, and setting the correct attributes. You can create the managed object with NSEntityDescription's -insertNewObjectForEntityForName:inManagedObjectContext: method.
Related
I'm trying to do the following - I have an Array in which some strings are stored. These strings shall be used to call an NSArray. An example will clarify what I'm trying to do:
This is the working code that I'm trying to achieve ("briefing0" is of type NSArray):
NSString *path = [docsDir stringByAppendingPathComponent:[briefing0 objectAtIndex:indexPath.row]];
This is the "same" code that I'm trying to use:
int i = 0;
NSString *path = [docsDir stringByAppendingPathComponent:[(NSArray *)[NSString stringWithFormat:#"briefing%d", i] objectAtIndex:indexPath.row]];
Any ideas?
Thanks in advance!
Tom
Assuming that briefing0 is actually a property, then yes, this is possible (and not uncommon) in ObjC via KVC.
int i = 0;
NSString *prop = [NSString stringWithFormat:#"briefing%d", i];
NSArray *array = [self valueForKey:prop];
NSString *value = [array objectAtIndex:indexPath.row];
... etc. ...
-valueForKey: is the piece you're looking for. Note that this will throw an exception if you construct a key that does not exist, and so must be used with extreme care.
I often use Transformable for Core Data attributes, so I can change them later.
However, it seems like, if I want to use NSPredicate to find a NSManagedObject, using "uniqueKey == %#", or "uniqueKey MATCHES[cd] %#", it's not working as it should.
It always misses matching objects, until I change the attributes of the uniqueKey of the matching object to have specific class like NSString, or NSNumber.
Can someone explain the limitation of using NSPredicate with Transformable attributes?
Note: I'm not sure when/if this has changed since 5/2011 (from Scott Ahten's accepted answer), but you can absolutely search with NSPredicate on transformable attributes. Scott correctly explained why your assumptions were broken, but if Can someone explain the limitation of using NSPredicate with Transformable attributes? was your question, he implied that it is not possible, and that is incorrect.
Since the is the first google hit for "Core Data transformable value search nspredicate" (what I searched for trying to find inspiration), I wanted to add my working answer.
How to use NSPredicate with transformable properties
Short, heady answer: you need to be smart about your data transformers. You need to transfrom the value to NSData that contains what I'll call "primitive identifying information", i.e. the smallest, most identifying set of bytes that can be used to reconstruct your object. Long answer, ...
Foremost, consider:
Did you actual mean to use a transformable attribute? If any supported data type -- even binary data -- will suffice, use it.
Do you understand what transformable attributes actually are? How they pack and unpack data to and from the store? Review Non-Standard Persistent Attributes in Apple's documentation.
After reading the above, ask: does custom code that hides a supported type "backing attribute" work for you? Possibly use that technique.
Now, past those considerations, transformable attributes are rather slick. Frankly, writing an NSValueTransformer "FooToData" for Foo instances to NSData seemed cleaner than writing a lot of adhoc custom code. I haven't found a case where Core Data doesn't know it needs to transform the data using the registered NSValueTransformer.
To proceed simply address these concerns:
Did you tell Core Data what transformer to use? Open the Core Data model in table view, click the entity, click the attribute, load the Data Model Inspector pane. Under "Attribute Type: Transformable", set "Name" to your transformer.
Use a default transformer (again, see the previous Apple docs) or write your own transformer -- transformedValue: must return NSData.
NSKeyedUnarchiveFromDataTransformerName is the default transformer and may not suffice, or may draw in somewhat-transient instance data that can make two similar objects be different when they are equal.
The transformed value should contain only -- what I'll call -- "primitive identifying information". The store is going to be comparing bytes, so every byte counts.
You may also register your transformer globally. I have to do this since I actually reuse them elsewhere in the app -- e.g. NSString *name = #"FooTrans"; [NSValueTransformer setValueTransformer:[NSClassFromString(name) new] forName:name];
You probably don't want to use transforms heavily queried data operations - e.g. a large import where the primary key information uses transformers - yikes!
And then in the end, I simply use this to test for equality for high-level object attributes on models with NSPredicates -- e.g. "%K == %#" -- and it works fine. I haven't tried some of the various matching terms, but I wouldn't be surprised if they worked sometimes, and others not.
Here's an example of an NSURL to NSData transformer. Why not just store the string? Yeah, that's fine -- that's a good example of custom code masking the stored attribute. This example illustrates that an extra byte is added to the stringified URL to record if it was a file URL or not -- allowing us to know what constructors to use when the object is unpacked.
// URLToDataTransformer.h - interface
extern NSString *const kURLToDataTransformerName;
#interface URLToDataTransformer : NSValueTransformer
#end
...
// URLToDataTransformer.m - implementation
#import "URLToDataTransformer.h"
NSString *const kURLToDataTransformerName = #"URLToDataTransformer";
#implementation URLToDataTransformer
+ (Class)transformedValueClass { return [NSData class]; }
+ (BOOL)allowsReverseTransformation { return YES; }
- (id)transformedValue:(id)value
{
if (![value isKindOfClass:[NSURL class]])
{
// Log error ...
return nil;
}
NSMutableData *data;
char fileType = 0;
if ([value isFileURL])
{
fileType = 1;
data = [NSMutableData dataWithBytes:&fileType length:1];
[data appendData:[[(NSURL *)value path] dataUsingEncoding:NSUTF8StringEncoding]];
}
else
{
fileType = -1;
data = [NSMutableData dataWithBytes:&fileType length:1];
[data appendData:[[(NSURL *)value absoluteString] dataUsingEncoding:NSUTF8StringEncoding]];
}
return data;
}
- (id)reverseTransformedValue:(id)value
{
if (![value isKindOfClass:[NSData class]])
{
// Log error ...
return nil;
}
NSURL *url = nil;
NSData *data = (NSData *)value;
char fileType = 0;
NSRange range = NSMakeRange(1, [data length]-1);
[data getBytes:&fileType length:1];
if (1 == fileType)
{
NSData *actualData = [data subdataWithRange:range];
NSString *str = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding];
url = [NSURL fileURLWithPath:str];
}
else if (-1 == fileType)
{
NSData *actualData = [data subdataWithRange:range];
NSString *str = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding];
url = [NSURL URLWithString:str];
}
else
{
// Log error ...
return nil;
}
return url;
}
#end
Transformable attributes are usually persisted as archived binary data. As such, you are attempting to compare an instance of NSData with an instance of NSString or NSNumber.
Since these classes interpret the same data in different ways, they are not considered a match.
you can try this way
NSExpression *exprPath = [NSExpression expressionForKeyPath:#"transformable_field"];
NSExpression *exprKeyword = [NSExpression expressionForConstantValue:nsdataValue];
NSPredicate *predicate = [NSComparisonPredicate predicateWithLeftExpression:exprPath rightExpression:exprKeyword modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0];
I need to see if an object that I have obtained from a CSV file with a unique identifier exists in my Core Data Database, and this is the code I deemed suitable for this task:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity;
entity =
[NSEntityDescription entityForName:#"ICD9"
inManagedObjectContext:passedContext];
[fetchRequest setEntity:entity];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"uniqueID like %#", uniqueIdentifier];
[fetchRequest setPredicate:pred];
NSError *err;
NSArray* icd9s = [passedContext executeFetchRequest:fetchRequest error:&err];
[fetchRequest release];
if ([icd9s count] > 0) {
for (int i = 0; i < [icd9s count]; i++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
NSString *name = [[icd9s objectAtIndex:i] valueForKey:#"uniqueID"];
if ([name caseInsensitiveCompare:uniqueIdentifier] == NSOrderedSame && name != nil)
{
[pool release];
return [icd9s objectAtIndex:i];
}
[pool release];
}
}
return nil;
After more thorough testing it appears that this code is responsible for a huge amount of leaking in the app I'm writing (it crashes on a 3GS before making it 20 percent through the 1459 items). I feel like this isn't the most efficient way to do this, any suggestions for a more memory efficient way? Thanks in advance!
Don't use the like operator in your request predicate. Use =. That should be much faster.
You can specify the case insensitivity of the search via the predicate, using the [c] modifier.
It's not necessary to create and destroy an NSAutoreleasePool on each iteration of your loop. In fact, it's probably not needed at all.
You don't need to do any of the checking inside the for() loop. You're duplicating the work of your predicate.
So I would change your code to be:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:...];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"uniqueID =[c] %#", uniqueIdentifier]];
NSError *err = nil;
NSArray *icd9s = [passedContext executeFetchRequest:fetchRequest error:&err];
[fetchRequest release];
if (error == nil && [icd9s count] > 0) {
return [icd9s objectAtIndex:0]; //we know the uniqueID matches, because of the predicate
}
return nil;
Use the Leaks template in Instruments to hunt down the leak(s). Your current code may be just fine once you fix them. The leak(s) may even be somewhere other than code.
Other problems:
Using fast enumeration will make the loop over the array (1) faster and (2) much easier to read.
Don't send release to an autorelease pool. If you ever port the code to garbage-collected Cocoa, the pool will not do anything. Instead, send it drain; in retain-release Cocoa and in Cocoa Touch, this works the same as release, and in garbage-collected Cocoa, it pokes the garbage collector, which is the closest equivalent in GC-land to draining the pool.
Don't repeat yourself. You currently have two [pool release]; lines for one pool, which gets every experienced Cocoa and Cocoa Touch programmer really worried. Store the result of your tests upon the name in a Boolean variable, then drain the pool before the condition, then conditionally return the object.
Be careful with variable types. -[NSArray count] returns and -[NSArray objectAtIndex:] takes an NSUInteger, not an int. Try to keep all your types matching at all times. (Switching to fast enumeration will, of course, solve this instance of this problem in a different way.)
Don't hide releases. I almost accused you of leaking the fetch request, then noticed that you'd buried it in the middle of the code. Make your releases prominent so that you're less likely to accidentally add redundant (i.e., crash-inducing) ones.
I have an NSMutableArray i am trying to convert into a string.
Declaring my NSMutableArray...
NSMutableArray *listData;
And later inside a method...
NSString *foo = [listData componentsJoinedByString:#"|"];
NSLog(#"%#",foo);
It seems no matter what i try i keep getting EXC_BAD_ACCESS.
To make sure each element in my array was an NSString i also tried this...
NSMutableArray *mArray = [[NSMutableArray alloc] init];
for (id ln in listData) {
NSString *boo = [NSString stringWithFormat: #"%#",ln];
[mArray addObject:boo];
}
NSString *foo = [mArray componentsJoinedByString:#"|"];
NSLog(#"%#",foo);
I can manipulate my NSMutableArray by adding/deleting objects in the same method or other methods inside my class. But when i try "componentsJoinedByString" the error pops up. Does anyone have any advice or another way i can combine this array into a single NSString?
In the code you've given, there will never be an NSMutableArray for listData. At some point in your code, you'll need to create one, and presumably populate it.
Edit
Okay, so you may get into memory management problems here, so let's be a bit clearer:
You're synthesizing getters and setters for the instance variable, so it's good practice to use those to access it, they'll take care of retain and releasing appropriately.
To set listData you can simply use
self.listData = [listManage getList:[[NSUserDefaults standardUserDefaults] stringForKey:#"list_name"] list:#"LIST"];
or
[self setListData:[listManage getList:[[NSUserDefaults standardUserDefaults] stringForKey:#"list_name"] list:#"LIST"]];
if you prefer.
Trying to create a Skat-Game, I encountered the following problem:
isBidding is a Boolean value indicationg, the program is in a certain state,
[desk selected] is a method calling returning the current selected player,
chatStrings consists of dictionaries, saving strings with the player, who typed, and what he typed
- (void)drawRect:(NSRect)rect{
NSMutableDictionary * attributes = [NSMutableDictionary dictionary];
[attributes setObject:[NSFont fontWithName:playerFont size:playerFontSize] forKey:NSFontAttributeName];
[attributes setObject:playerFontColor forKey:NSForegroundColorAttributeName];
[[NSString stringWithFormat:#"Player %i",[desk selected] + 1] drawInRect:playerStringRect withAttributes:attributes];
if (isBidding){
[attributes setObject:[NSFont fontWithName:chatFont size:chatFontSize] forKey:NSFontAttributeName];
[attributes setObject:chatFontColor forKey:NSForegroundColorAttributeName];
int i;
for (i = 0; i < [chatStrings count]; i++, yProgress -= 20){
if (isBidding)
[[NSString stringWithFormat:#"Player %i bids: %#",
[[[chatStrings objectAtIndex:i]valueForKey:#"Player"]intValue],
[[chatStrings objectAtIndex:i]valueForKey:#"String"]],
drawAtPoint:NSMakePoint([self bounds].origin.x, yProgress) withAttributes:attributes];
else
[[NSString stringWithFormat:#"Player %i: %#",[[[chatStrings objectAtIndex:i] valueForKey:#"Player"]intValue],
[[chatStrings objectAtIndex:i]valueForKey:#"String"]]
drawAtPoint:NSMakePoint([self bounds].origin.x, yProgress) withAttributes:attributes];
}
}
if (isBidding)
[[NSString stringWithFormat:#"Player %i bids: %#",[desk selected] + 1, displayString]
drawAtPoint:NSMakePoint([self bounds].origin.x, yProgress) withAttributes:attributes];
else
[[NSString stringWithFormat:#"Player %i: %#",[desk selected] + 1, displayString]
drawAtPoint:NSMakePoint([self bounds].origin.x, yProgress) withAttributes:attributes];
yProgress = chatFontBegin;
}
This is the part determining the string's content, the string is contributed by an [event characters] method.
-(void)displayChatString:(NSString *)string{
displayString = [displayString stringByAppendingString:string];
[self setNeedsDisplay:YES];
}
The problem if have is this:
when typing in more than two letters the view displays NSRectSet{{{471, 574},{500, 192}}}
and returns no more discription when I try to print it.
then I get an EXC_BAD_ACCESS message, though I have not released it (as far as I can see) I also created the string with alloc and init, so I cannot be in the autorelease pool.
I also tried to watch the process when it changes with the debugger, but I couldn't find any responsible code.
As you can see I am still a beginner in Cocoa (and programming in general), so I would be really happy if somebody would help me with this.
This code is buggy:
-(void)displayChatString:(NSString *)string{
displayString = [displayString stringByAppendingString:string];
[self setNeedsDisplay:YES];
}
stringByAppendingString: returns an autoreleased object. You need to retain or copy it if you want it to stick around (maybe by using a copying/retaining property like self.displayString = [displayString stringByAppendingString:string]; and the matching property declaration/synth.
So at the moment, you are assigning an object which will be deallocated, but you later access it giving the error.
I can't puzzle out your code but I can tell you something about the odd return.
NSRectSet is a private type inside the Foundation framework. You shouldn't ever see it. Its used internally IIRC to represent nested rectangles such as those of stacks of views.
You're either getting an odd memory problem that causes a string pointer to actually point at the NSRectSet or you've screwed up your method nesting and you're assigning the NSRectSet value to a string.