I'm in the midst of writing a data import routine for an iPad application. It seems to be working fine. However, because I'm an IOS noob and coming from an application development background in which such tasks are handled via calls to an SQL server, I'm feeling the need to double-check if I'm using the correct approach.
Here is the basic approach I'm using to import data into one of my master data entities:
// SET UP SOME VARIABLES/VALUES APPLICABLE TO ALL IMPORTS
NSStringEncoding encoding = 1;
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
NSString* file = nil;
NSError* error = nil;
NSArray* records = nil;
NSArray* fields = nil;
NSPredicate* predicate = nil;
NSArray* filteredArray = nil;
// IMPORT GEO DATA
// ProvinceState
file = [[NSBundle bundleForClass:[self class]] pathForResource:#"ProvinceState" ofType:#"txt"];
error = nil;
records = [NSArray arrayWithContentsOfCSVFile:file usedEncoding:&encoding error:&error];
NSManagedObject* provinceState = nil;
NSMutableArray* provinceStateArray = [NSMutableArray arrayWithCapacity:[records count]];
for (int i = 0; i < [records count]; ++i)
{
fields = [records objectAtIndex:i];
if ([fields count] == 2)
{
provinceState = [NSEntityDescription insertNewObjectForEntityForName:#"ProvinceState" inManagedObjectContext:self.managedObjectContext];
[provinceState setValue: [numberFormatter numberFromString:[fields objectAtIndex:0]] forKey:#"id"];
[provinceState setValue:[fields objectAtIndex:1] forKey:#"name"];
[provinceStateArray addObject:provinceState];
}
}
A couple of key points. This particular entity is very simple - just an integer id and a string name. Its source table is a simple csv file. The method arrayWithContentsOfCSVFile:usedEncoding:error: is from Dave Delong's excellent CHCSV parser, which makes parsing a breeze. I have not yet applied any error handling because I'm trying to work out the basic techniques first.
Okay, so unless you spot anything wrong with this simple code, I'll now get on with my main questions. The provinceStateArray referenced in the last meaningful line of the above code is to save myself the hassle of having to query the master data entities later on, when I need to populate them into the relationships of my transactional data. I realize this a) increases my memory requirements and b) prevents me, by implication, from draining the associated autoreleasepool, but since the half-dozen or so master data entities contain only a few hundred small objects in total, I didn't think this was a concern. Anyone think differently?
Now the part that does concern me. In the following code, which populates a Locations entity, I'm using these same master data entity arrays as follows:
file = [[NSBundle bundleForClass:[self class]] pathForResource:#"Location" ofType:#"txt"];
error = nil;
records = [NSArray arrayWithContentsOfCSVFile:file usedEncoding:&encoding error:&error];
NSManagedObject* location = nil;
NSMutableArray* locationArray = [NSMutableArray arrayWithCapacity:[records count]];
for (int i = 0; i < [records count]; ++i)
{
NSArray* fields = [records objectAtIndex:i];
if ([fields count] == 5)
{
location = [NSEntityDescription insertNewObjectForEntityForName:#"Location" inManagedObjectContext:self.managedObjectContext];
[location setValue: [numberFormatter numberFromString:[fields objectAtIndex:0]] forKey:#"id"];
predicate = [NSPredicate predicateWithFormat:#"id = %#", [numberFormatter numberFromString:[fields objectAtIndex:1]]];
filteredArray = [provinceStateArray filteredArrayUsingPredicate:predicate];
if ([filteredArray count] > 0)
{
[location setValue:[filteredArray objectAtIndex:0] forKey:#"provinceState"];
}
...and so on down through the other relationship entities that must be set.
This is the part - having to construct large quantities of objects simply to set the relationships - that feels foreign to me, even though I don't see any alternative.
Also, because the source data is from an RDBMS, I'm populating the one side of one-to-many relationships instead of iterating through sets.
Anything smell bad in all of this, or can I carry on with some comfort that I understand?
I think it is better to create SQLite database, import it to your project and import data from a file to that database(it is hard drive memory), not to arrays in RAM. Then, when your database is ready for use, make queries to it to populate only needed data for your user to iPhone RAM.
Related
I have an NSArray of NSDictionaries in my app. In each dictionary I hold an NSDate called "RunDate." The problem I am having now is that the code I am trying to do it with is very inefficient. Basically I only want one section per date out of all the dictionaries. Then in each section (sorted by that date), I would load the appropriate dictionary that had that date.
In the code below I made a new NSArray of NSDictionarys which held a date and number of that date (so I could know how many rows are in each section). The problem is, this code looks and feels very inefficient and I was wondering if there were any ways my code is incorrect or could be improved upon. There can be over 500 entries and the code I have now would be very slow. Does anyone have any suggestions on it?
runArray = [[NSMutableArray alloc] init];
runArray = [[[NSUserDefaults standardUserDefaults] arrayForKey:#"RunArray"] mutableCopy];
runDictTableArray = [[NSMutableArray alloc] init];
for (NSDictionary *dict in runArray) {
NSDictionary *runInfoDict = [[NSMutableDictionary alloc] init];
NSDate *theDate = [dict objectForKey:#"RunDate"];
//Check if we already have this date in the saved run array
BOOL goToNextDict = FALSE;
for (NSDictionary *savedDict in runDictTableArray) {
if ([theDate compare:[savedDict objectForKey:#"RunDate"]] == NSOrderedSame) {
goToNextDict = TRUE;
break;
}
}
if (goToNextDict)
continue;
////////////////////////////
//Now we check how many more we have of this date
int numbOfDate = 1;
int index = (int)[runArray indexOfObject:dict];
for (int i = index; i < [runArray count]; i++) {
NSDictionary *dictInner = [runArray objectAtIndex:i];
if ([theDate compare:[dictInner objectForKey:#"RunDate"]] == NSOrderedSame) {
numbOfDate++;
}
}
////////////////////////////
[runInfoDict setValue:[NSNumber numberWithInt:numbOfDate] forKey:#"DateAmount"];
[runInfoDict setValue:theDate forKey:#"Date"];
[runDictTableArray addObject:runInfoDict];
}
Some suggestions:
You probably only need 1 NSMutableDictionary, rather than a NSMutableArray of NSDictionary. While looping through runArray, check if your dictionary has a value for your date (objectForKey returns a value). If it does, add 1 to the count. If it does not, add that date as a key to the dictionary with a value of 1. This way, you won't have to do the inner loop to get the number of times a date occurs. You won't need the 'go to next dictionary' logic either, I would think.
runArray = [[NSMutableArray alloc] init]; doesn't really do anything since you're immediately re-assigning runArray.
Consider using NSInteger over regular int, NSInteger will give you the appropriate size for whatever architecture your app is running on.
There's some cool syntax shortcuts you might like. You can avoid [runInfoDict setValue:[NSNumber numberWithInt:numbOfDate]... by simply writing [runInfoDict setValue:#(numbOfDate) ..., which will put the value into NSNumber for you.
I have two entities, one called InProject that has several attributes and one relationship. the relationship is with another entity called Ins.
I am editing one of the Ins that is related to InProject. I used InProject attribute ID which then returns a NSDictionary value that has several key-values one of which is for an array of Ins. I then find the Ins I need to edit in a for loop I edit them, but then I become unstuck because I am not sure how to save the contect of InProject with the *updated Ins
I need to figure out how to save InProject after I have overwritten the Ins attributes I need to update.
This is what my code looks like after battling this problem:
- (void)editSelectedins:(NSString *)projIDString UpdatedNSD:(NSMutableDictionary *)updatedNSD DPC:(int)dpc{
// get context
NSManagedObjectContext *context = [self managedObjectContext];
if (context == nil) {
NSLog(#"Nil");
}
else {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"InsProject" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error;
NSMutableArray *InsProjectDictionaryArray = [[NSMutableArray alloc] init];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
for (InsProject *insProj in fetchedObjects) {
NSMutableDictionary *tempInsProjectDictionaryArray = [[ NSMutableDictionary alloc] init];
[tempInsProjectDictionaryArray setObject:insProj.companyName forKey:#"CompanyName"];
[tempInsProjectDictionaryArray setObject:insProj.projNo forKey:#"ProjNo"];
[tempInsProjectDictionaryArray setObject:insProj.desc forKey:#"Desc"];
[tempInsProjectDictionaryArray setObject:insProj.guid forKey:#"GUID"];
[tempInsProjectDictionaryArray setObject:insProj.projID forKey:#"ProjID"];
[tempInsProjectDictionaryArray setObject:insProj.ins forKey:#"ins"];
[InsProjectDictionaryArray addObject:tempInsProjectDictionaryArray];
}
// now that you have the InsProjects, choose the one you are curently working on in insView using the projectID
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ProjID==%#",projIDString];
[fetchRequest setPredicate:predicate];
// new array with one value that was created using the NSPredicate ProjID
NSArray *tempInsProjectArray = [InsProjectDictionaryArray filteredArrayUsingPredicate:predicate];
// get ins array out of the NSDictionary to edit
NSSet *inssForInsProject = tempInsProjectArray[0][#"ins"];
NSMutableArray *tempAllinss = [[NSMutableArray alloc] init]; // this will contain everything, that means all repeated values are included
for (Items* currItem in [inssForInsProject allObjects]) {
NSArray *keys = [[[currItem entity] attributesByName] allKeys];
NSDictionary *dict = [currItem dictionaryWithValuesForKeys:keys];
[tempAllinss addObject:dict];
}
NSArray *myArray = [tempAllinss copy];
// get the correct items from myArray anything whos dpc matches the dpc parameter of this method
NSMutableArray *editedinsArray = [[NSMutableArray alloc] init];
for (int i = 0; i < [myArray count]; i++) {
NSMutableDictionary *tempinssDictionary = [myArray objectAtIndex:i];
// if you get a match put it into the new editedinsArray to be edited
if ([[tempinssDictionary objectForKey:#"dpc"] integerValue] == dpc) {
[editedinsArray addObject:tempinssDictionary];
}
}
// by now you should have three things
// 1, access to your ins coredata object //this s wrong I actually have access to insProject
// 2, the values you need to be edited saved into a NSArray (editedinsArray, which will be used to check against and keep old values correct)
// 3, UpdatedNSD which will be used to update any values that need to be updated.
// go through your values and update the ins object
int i = 0;
for (ins *temp in editedinsArray) {
NSDictionary *currentEditedins = [editedinsArray objectAtIndex:i];
i++;
// these values should stay the same so use currentEditedins which contains old vals
NSString *stringToNumberDpc = [currentEditedins valueForKey:#"dpc"];
int tempDpcNum = [stringToNumberDpc integerValue];
NSNumber *dpcNumber = [NSNumber numberWithInt:tempDpcNum];
temp.dpc = dpcNumber;
NSString *totDQtyString = [currentEditedins valueForKey:#"totDQty"];
if ((NSNull *)totDQtyString == [NSNull null]) {
temp.totDQty = #"";
} else {
temp.totDQty = totDQtyString;
}
NSString *totShipString = [currentEditedins valueForKey:#"totShip"];
if ((NSNull *)totShipString == [NSNull null]) {
temp.totShip = #"";
} else {
temp.totShip = totShipString;
}
// values to be updated so use updatedNSD wthich was passed in as method param with the new vals
temp.newInsComp = [updatedNSD valueForKey:#"newInsComp"];
temp.newDryComp = [updatedNSD valueForKey:#"newDryComp"];
temp.updatedRow = [updatedNSD valueForKey:#"updatedRow"];
}
#warning --- I have no idea what to do here... i.e. how do I update the tempInsProjectArray.ins values I have just updated in the above for loop then save context which I hope would update insProj and the ins entities involved.
//save
[context save:&error];
}
}
As you can see at the bottom of the code with #warning I explain where I am having the issue. if I log temp inside the for loop I see the updated values perfectly the issue I am having is how do I then update the current tempInsProjectArray.ins values that I have just edited? then save them of course.
Your code is in great need of simplification. Some ground rules:
Use names with smallInitial and camelCase for variables. So not InsProjectDictionaryArray but insProjectDictionaryArray.
The same applies to dictionary keys indicating attribute names of managed objects. So projNo, not ProjNo.
Avoid cryptic abbreviations. Use plain and readable English Not projNo but projectNumber. What is an Ins? What is "dcp"?
Don't use the plural form for entity names. An suitable name for an item is Item, not Items
Don't use the mutable versions of dictionary and array when immutable ones would do.
Avoid duplicating your data, such as in [array copy].
Avoid dictionaries when you have an object graph. The object graph is what core data creates. It renders dictionaries with values and keys unnecessary.
Don't use IDs. The object graph renders those unnecessary as well in most cases. If you use IDs, do not use strings but numbers, such as long ints, or the object version NSNumber.
When fetching data from the Core Data persistent store, don't fetch all the data and the filter the result. Fetch only the data you need.
What you want to accomplish can surely be done in a few lines of code. I will try to summarize what you want to do as far as I understand it.
Your data model looks something like this:
Project <----->> Item
Where the items are in a to-many relationship called ins. I will rename this items. I will also assume that you will refactor your IDs to be of type NSNumber.
All the code up to myArray could be substituted with this:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:"Project"];
request.predicate = [NSPredicate predicateWithFormat:#"projectID = %#", projectID];
request.fetchLimit = 1;
NSArray *fetchedObjects = [self.managedObjectContext
executeFetchRequest:request error:nil];
Project *project = fetchedObjects[0];
You now have all items available simply with project.items. I understand that there could be more than one item with a mysterious attribute dcp of type int (i.e. NSNumber for managed objects), that is equal to the dcp parameter passed.
NSSet *matchingItems = [project.items filteredSetUsingPredicate:
[NSPredicate predicateWithFormat:#"dcp = %#", #(dcp)]];
Now it becomes a bit murky. Why do you have type ins in your for loop if the ins are actually of type Item? You then cast them into a dictionary... This should generate a compiler error. Or you have another class called ins instead of Ins??
Anyway, if you stay with the Items you can just update the values with what you pass in your dictionary:
for (Item *item in matchingItems) {
item.newInsComp = [updatedNSD valueForKey:#"newInsComp"];
item.newDryComp = [updatedNSD valueForKey:#"newDryComp"];
item.updatedRow = [updatedNSD valueForKey:#"updatedRow"];
}
[self.managedObjectContext save:nil];
Done!
BTW you could make it even shorter by setting the entity name of the fetch request to "Item" and setting the following predicate:
[NSPredicate predicateWithFormat:#"project.projectID = %# && dcp = %#",
projectID, #(dcp)];
If you know your InProject, then updating your Ins related to that project is a matter of editing property values on your managed objects.
Why not use the predicate to get an NSManagedObject of the InProject, then pull the relationship off of that and edit the values?
NSManagedObjectContext *context = [self managedObjectContext];
if (!context) {
return;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"InsProject" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Set the predicate on the Core Data fetch request instead
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"ProjID==%#",projIDString];
NSError *error;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
// We now have an array that has objects matching the projectIdString
// Might want to do some additional checks if you're only expecting zero or one objects
InsProject *aProject = [fetchedObjects lastObject];
// If we have no project, no point going any further
if ( !aProject ) return;
// On this NSManagedObject is an NSSet property with all related Ins objects
for ( Ins *anIns in aProject.ins ) {
// If our Ins item matches the passed dpc...
if ( [ins.dpc integerValue] == dpc ) {
// ...we have a match, edit properties
ins.dpc = #(dpc);
ins.newInsComp = [updatedNSD valueForKey:#"newInsComp"];
ins.newDryComp = [updatedNSD valueForKey:#"newDryComp"];
ins.updatedRow = [updatedNSD valueForKey:#"updatedRow"];
}
}
// These are managed objects, so saving the context saves all the changes
NSError *saveError;
[context save:&saveError];
if ( saveError ) {
NSLog(#"Save error: %#", [error localizedDescription]);
}
I am using core data and have been using code like this:
[self.form setValue:self.comments.text forKey:#"comments"];
I want to put code like this into a loop, all my coredata names are the same as the property name. How can I say forKey:self.comments.name and get the same outcome as above or something like that?
EDIT:
If this is not possible, is there another way to set a ton of values into coredata from properties? I have 50+ attributes and properties alike that need to be set and would like to avoid using what im doing now.
If you really want it, you may use these functions from objc/runtime.h:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) // To get properties declared by a class.
const char *property_getName(objc_property_t property) // To get the name of one property
Something like this:
unsigned int propCount = 0;
objc_property_t *properties = class_copyPropertyList([self class], &propCount);
for(int idx = 0; idx < propCount; idx++) {
objc_property_t prop = *(properties + idx);
NSString *key = #(property_getName(prop));
NSLog(#"%#", key);
}
There really is no substitute for reading the docs on CoreData as the patterns for use and syntax will not be obvious at all without a little legwork.
That said, you typically fetch an instance of your NSManagedObject subclass from the data store:
NSManagedObjectContext* moc = [delegate managedObjectContext];
NSEntityDescription* description = [NSEntityDescription entityForName:#"Filter" inManagedObjectContext:moc];
NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:description];
[request setSortDescriptors:[NSArray arrayWithObject:descriptor]];
NSError *error;
_enabledFilters = [NSMutableArray arrayWithArray:[moc executeFetchRequest:request error:&error]];
if (error) {
NSLog(#"%#",error.localizedDescription);
}
In this example I now have an array of instances of my NSManagedObject called "Filter"
Then you can select the appropriate instance to reference, and access all of it's attributes with simple dot syntax.
Filter* thisFilter = (Filter*)[_displayFilters objectAtIndex:indexPath.row];
cell.label.text = thisFilter.name;
cell.label.backgroundColor = [UIColor clearColor];
NSString*targetName = thisFilter.imageName;
UIImage *image = [UIImage imageNamed:targetName];
cell.image.image = image;
Now I've taken info from my persistent data store, and used it within my app.
Going the other way and writing to an instance within your data store is only slightly different, in that you directly set the attributes of an instance of your NSManagedObject subclass, and then call save on the context to push any changes down to the store.
TL;DR - you owe it to yourself to spend an hour or two with the CoreData docs...
One way would be to declare an array of the attributes yourself.
NSArray *attributes = [NSArray arrayWithObjects:..., #"comments", .., nil]; // or a NSSet
for(NSString *attribute in attributes){
NSString *text = [[self performSelector:NSSelectorFromString(attribute)] text]; // presuming that it's safe to call 'text' on all your properties
[self.form setValue:text forKey:attribute];
}
Or you can use this if you want all the attributes of your core data model.
This question already has answers here:
Removing duplicates from NSMutableArray
(9 answers)
Closed 10 years ago.
I have an NSMutableArray which has entity class object as its objects.
Now I want to remove the distinct objects from it. Consider following example
Entity *entity = [[Entity alloc] init];
entity.iUserId = 1;
entity.iUserName = #"Giri"
[arr addObject:entity];
Entity *entity = [[Entity alloc] init];
entity.iUserId = 2;
entity.iUserName = #"Sunil"
[arr addObject:entity];
Entity *entity = [[Entity alloc] init];
entity.iUserId = 3;
entity.iUserName = #"Giri"
[arr addObject:entity];
Now I want only two objects in the Array by removing the duplicate iUserName. I know the way by iteration but I want it without iterating it like predicate or some other way.
If anyone knows then please help me.
I had tried using [arr valueForKeyPath:#"distinctUnionOfObjects.iUsername"]; but it does not return me the entired object.
This question is totally different than the questions which are asked previously. Previously asked question is for getting the distinct objects is correct but they uses looping & I don't want this. I want it from NSPredicate or any other simple option which avoids looping.
EDIT: You can't do what you want to without looping over the array manually and building up a new array. The answer below won't work because it assumes that there are only at most two duplicates.
NSMutableArray *filteredArray = [NSMutableArray array];
for (Entity *entity in arr)
{
BOOL hasDuplicate = [[filteredArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"iUserName == %#", entity.iUserName]] count] > 0;
if (!hasDuplicate)
{
[filteredArray addObject:entity];
}
}
This will look for duplicates in the filtered array as it builds it.
Begin Original Answer
You can't use an NSSet because the Entity instances would have to return NSOrderedSame in compareTo:, which isn't a good idea since you shouldn't use names as unique identifiers.
You can use predicates, but they'll still loop over the array in an O(n^2) time without some optimization.
NSArray *filteredArray = [arr filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Entity *evaluatedObject, NSDictionary *bindings) {
return [[arr filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"iUserName == %#", evaluatedObject.iUserName]] count] > 1;
}]];
That'll work fine. You could make it even faster by sorting the array by the iUserName property first and doing a linear scan over the sorted array (stopping when you see the first duplicate). That's a lot of work if you're dealing with small sample sizes (say, under ten thousand or so). It's probably not worth your time, so just use the code above.
Well you have a few options (that I can think of).
Use a NSSet instead of a NSArray.
Use a for loop (but you don't want to iterate through the array)
Use a predicate search iUserName to see if the name exists before adding it to the array.
Something like:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"iUserName == 'Giri'"];
NSArray *searchArray = [arr filterUsingPredicate:predicate];
I have two tables that I would like to get data and from by seeing if they match a particular attribute.
I want to write a predicate or something like this but I do not seem to be able to come up with a solution: #"Data.item == Item.code".
The reason why I don't use relationships is because my database was imported from mysql. So all the data is coming from outside of the app its being synced from downloaded mysql tables.
---------------------------EDIT-------------------------
What I have tried so far lots of things here is the crappy way I am doing this now perhaps from this you can understand more of what I am trying to do .
NSPredicate * Newpredicate = [NSPredicate predicateWithFormat:#"hid == 2"];
NSArray *row2 = [db DLook:Newpredicate table:#"Data"];
for (NSManagedObject *data in row2) {
NSLog(#"\n\n\n\nid\n\n\n\n: %#", [data valueForKey:#"id"]);
NSString *itemToCode = [data valueForKey:#"item"];
NSPredicate *itemPredicate = [NSPredicate predicateWithFormat:#"code == %#",itemToCode];
NSArray *itemRow = [db DLook:itemPredicate table:#"Item"];
for (NSManagedObject *item in itemRow) {
NSLog(#"\n\n\n\ncode : %#\n\n\n\n",[item valueForKey:#"code"]);
}
// NSLog(#"id: %#", [data valueForKey:#"id"]);
//NSManagedObject * itemhid= [data valueForKey:#"testRel"];
//NSLog(#"code: %#",[itemhid valueForKey:#"code"]);
}
NSLog(#"\n\n\n\n%d\n\n\n\n",[row2 count]);
The DLook is a convince method that just fetches the data using the predicate on the table that I pass. Then take the returned area of NSmanaged objects looping through them.
I wish I could just make a magical relationship that would let me get a all the Item.data that match the Data.items!!!
I don't want to do it like this I want to make a relationship that would work like that.
Help
Thanks
Your equality in the predicate would only be true if the two objects are actually the same object. You could do this:
[NSPredicate predicateWithFormat:#"item = %#", code];
However, you should really try to get the relationships right when importing your model. Then you would not even have to do a fetch from the store but just use
dataObject.item;
So, when you import do something like this:
// get one data object from your source
// get the corresponding item object from your source
Data *dataObject = [NSEntityDescription insertNewObjectForEntityForName:#"Data"
inManagedObjectContext:self.managedObjectContext];
Item *itemObject = [NSEntityDescription insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:self.managedObjectContext];
[dataObject addItemObject:itemObject];