I have an iOS app that utilizes RestKit 0.20.1 to retrieve data from a Restful web service. I should also add the app uses CoreData. When the app is started the main screen is a collection view that is populated by a default search term. There is also a textfield in the header that is used as a search bar.
I am having trouble clearing out the previously loaded cells when the user uses the search bar. It just loads the new cells and pushes the previous ones down. Here is the applicable code.
- (BOOL) textFieldShouldReturn:(UITextField *)textField {
//This sets up the NSDictionary for the Restkit postObject parameters
NSArray *objects =[NSArray arrayWithObjects:textField.text, nil];
NSArray *keys =[NSArray arrayWithObjects: #"query",nil];
NSDictionary *params = [NSDictionary dictionaryWithObjects:objects forKeys:keys];
self.search=params;
//This is the POST request to the server
[[RKObjectManager sharedManager] postObject:nil path:#"/rest/search?ip=255.255.255.0" parameters:search success:nil failure:nil];
//This is what I thought would clear out the old and replace with the new
[self.collectionView reloadData];
[textField resignFirstResponder];
return YES;
}
I referenced this question How to remove all items and add new items to UICollectionView? and [collectionView reloadData] was the accepted answer.
I chose the textFieldShouldReturn: method from this tutorial.
I have also referenced Inserting, Deleting, and Moving Section Items in the apple developer library but I'm not quite sure how to implement the delete items methods.
Any help would be great. This is clearly a rookie question so code snippets are VERY helpful.
UPDATE:
Here is how I got it to work.
Added a call to the deleteAllEntitiesForName method shown below before the postObject method and [self.collectionView reloadData] statements in the code above.
- (void) deleteAllEntitiesForName:(NSString*)entityName {
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:entityName inManagedObjectContext:moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array != nil) {
for(NSManagedObject *managedObject in array) {
[moc deleteObject:managedObject];
}
error = nil;
[moc save:&error];
}
}
I got it from here: Correct way to refresh Core Data database
From experience with UICollectionView (not with RestKit), you probably need to clear out your data source before calling reloadData. The reason you're having this effect is that the source that you use to determine the number of items and sections in the collection view still has old data in it.
A simple way to verify this is to NSLog the contents of the data source just before calling reloadData.
Hope this helps!
If you are using NSMutableArray then you can simply do:
[array_name removeAllObjects];
Do this before adding new objects to the array.
Related
I am working on an app which makes use of Core Data. I am using parent/child contexts to access Core Data from both the main and background threads. The application makes use of Quickblox framework (don't bother if you aren't aware of this framework, you need not be aware of it to answer the question), I have to use the API of this framework which makes a call to a server, fetches data and hands the control back to me using a block. Control is always returned back on the main thread i.e the block always executes on the main thread.
Here's my code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
QBResponsePage *page = [QBResponsePage responsePageWithLimit:ResponseLimit skip:0];
[QBRequest messagesWithDialogID:weakSelf.dialog.id
extendedRequest:extendedRequest
forPage:page
successBlock:^(QBResponse *response, NSArray *messages, QBResponsePage *page)
{
if(messages.count > 0)
{
NSArray *filteredArray=nil;
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Dialog"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"id == %#",weakSelf.dialog.id];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
Dialog *parentDialog=nil;
if([results count]>0){
parentDialog=[results objectAtIndex:0];
}
for(QBChatMessage *message in messages)
{
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Chats"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"chat_Id == %#",message.ID];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
if([results count]==0){
if([message.dateSent compare:parentDialog.last_Message_Date]==NSOrderedDescending){
parentDialog.last_Message_Date=message.dateSent;
parentDialog.last_Message_User_Id=[NSNumber numberWithInteger:message.senderID];
parentDialog.lastMessage=message.text;
}
Chats *recievedChat=[NSEntityDescription insertNewObjectForEntityForName:#"Chats" inManagedObjectContext:weakAppDelegate.managedObjectContext];
recievedChat.sender_Id=[NSNumber numberWithInteger:message.senderID];
recievedChat.date_sent=message.dateSent;
recievedChat.chat_Id=message.ID;
recievedChat.belongs=parentDialog;
recievedChat.message_Body=message.text;
}
}
//commit main context
}
//update user profilepic
} errorBlock:^(QBResponse *response) {
//handle error
}];
});
Everything works fine, but as soon as the statement recievedChat.belongs=parentDialog; gets executed, dealloc never gets called even after popping out the view controller containing code. Commented out this statement, obviously Core Data relationships won't work but dealloc gets called as expected.
I am confused. Please help guys.
Thanks in advance.
What are the best practices on where to update my Core Data?
The first guy who worked on this project I'm working right now created all the Core Data related functions inside the ViewController, but I wanted to declare them inside the model classes (NSManagedObject subclass) to separate concerns.
The main function is a AFNetworking postPath that calls a web service and returns an array of objects to add/edit/delete. What I did was create a class method and do this AFNetwork call inside it:
+ (void)updateEbooksListWithSuccessBlock:(void (^)())successBlock andFailureBlock:(void (^)())failureBlock {
NSURL *url = urlSchema (urlWebServices, #"");
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSString *postPath = [NSString stringWithFormat:#"ws-ebooks-lista.php"];
[httpClient postPath:postPath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([operation isKindOfClass:[AFHTTPRequestOperation class]]) {
NSDictionary *result = [[responseObject objectFromJSONData] retain];
bool success = statusDoRetornoDoWebService(result); //Function that checks if the return was successful
//Configura o Core Data
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSManagedObjectContext *localManagedObjectContext = [[NSManagedObjectContext alloc] init];
[localManagedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Ebooks" inManagedObjectContext:localManagedObjectContext];
NSPredicate *filterPredicate;
[request setEntity:entity];
if (success) {
NSArray *ebookInfos = [result objectForKey:#"saida"];
Ebooks *ebook;
NSManagedObject *objectInsert;
for (NSDictionary* ebookInfo in ebookInfos) {
filterPredicate = [NSPredicate predicateWithFormat:#"ebooks_id == %#",[ebookInfo valueForKey:#"id_ebook"]];
[request setPredicate:filterPredicate];
ebook = [[localManagedObjectContext executeFetchRequest:request error:&error] lastObject];
objectInsert = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:localManagedObjectContext];
if (ebook) {
if (![[ebookInfo valueForKey:#"excluido"] isEmpty]) {
//Delete Ebook
} else {
//Update Ebook
}
} else {
//Add Ebook
}
if (![localManagedObjectContext save:&error]) {
//Log Error
}
[objectInsert release];
}
}
[request release];
[localManagedObjectContext release];
}
[successBlock invoke];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//Failure
[failureBlock invoke];
}];}
And it works fine while the app is running, but if I close it (through Xcode) and open it again, the changes aren't saved. I tried not using the "parent context" way and just using the AppDelegate managed object context (since AFNetworking callbacks always runs on the main queue) but no success: the data is not persisted. Why is that? Am I doing something wrong? Is it bad practice? Should I leave everything in the View Controller the way it was?
Thanks!
I think it is a bad idea to have too much logic that ultimately relates to your data model into your entity classes. These tasks simply do not belong there. The entity classes should focus only on what they encapsulate: the entity instances themselves.
To illustrate: think of a class that represents a number (like NSNumber). It think it is not convenient to extend it to give you, say, an array of all even numbers within a certain limits, or the nth member of the Fibonacci series. It seems unsound to have a number class be responsible for saving itself to a file, or retrieving information from the web.
For these and similar reasons, I believe the fetching and saving of Core Data entities belongs into controllers, not entity classes. Remember, one of the basic ideas behind the MVC (model-view-controller) pattern is that the controller manipulates the model or asks it for information, not that the model manipulates itself.
I speculate that your troubles are derived mainly from not separating the various functional aspects of your application sufficiently (data model, persistence, network operations, user interactions).
ugh... what I would do is make very naked NSManagedObject subclasses... then extend them with categories, that way when you regenerate your classes from the updated model you don't have to try to merge in all of your custom logic.
also the custom logic belongs in the model, the model contains the category or class extension.
so take that crap out of the View Controllers and put it in an easily maintainable category or several categories if it is warranted.
Let's say I load in 1,000 objects via Core Data, and each of them has a user-settable Favorite boolean. It defaults to NO for all objects, but the user can paw through at will, setting it to YES for as many as they like. I want a button in my Settings page to reset that Favorite status to NO for every single object.
What's the best way to do that? Can I iterate through every instance of that Entity somehow, to set it back? (Incidentally, is 'instance' the right word to refer to objects of a certain entity?) Is there a better approach here? I don't want to reload the data from its initial source, since other things in there may have changed: it's not a total reset, just a 'Mass Unfavourite' option.
Okay, I've gotten it to work, but it requires a restart of the app for some reason. I'm doing this:
- (IBAction) resetFavourites: (id) sender
{
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
[fetch setEntity: [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext]]];
NSArray *results = [[[FQCoreDataController sharedCoreDataController] managedObjectContext] executeFetchRequest: fetch error: nil];
for (Quote *quote in results) {
[quote setIsFavourite: [NSNumber numberWithBool: NO]];
}
NSError *error;
if (![self.fetchedResultsController performFetch: &error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
}
This works fine if I close and re-open the app, but it isn't reflected immediately. Isn't that what the reloadData should do, cause an immediate refresh? Am I missing something there?
I am using a NSFetchResultsController to display 100,000 + records in a UITableView. This works but it is SLOW, especially on an iPad 1. It can take 7 seconds to load which is torture for my users.
I'd also like to be able to use sections but this adds at least another 3 seconds onto the laod time.
Here is my NSFetchResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (self.clientsController != nil) {
return self.clientsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Client" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
[request setPredicate:[NSPredicate predicateWithFormat:#"ManufacturerID==%#", self.manufacturerID]];
[request setFetchBatchSize:25];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"UDF1" ascending:YES];
NSSortDescriptor *sort2= [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObjects:sort, sort2,nil]];
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:#"Name", #"ManufacturerID",#"CustomerNumber",#"City", #"StateProvince",#"PostalCode",#"UDF1",#"UDF2", nil];
[request setPropertiesToFetch:propertiesToFetch];
self.clientsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
return self.clientsController;
}
I have an index on ManufacturerID which is used in my NSPredicate. This seems like a pretty basic NSFetchRequest - anything I can do to speed this up? Or have I just hit a limitation? I must be missing something.
First: you can use the NSFetchedResultsController's cache to speed up display after the first fetch. This should quickly go down to a fraction of a second.
Second: you can try to display the only the first screenful and then fetch the rest in the background. I do this in the following way:
When the view appears, check if you have the first page cache.
If not, I fetch the first page. You can accomplish this by setting the fetch request's fetchLimit.
In case you are using sections, do two quick fetches to determine the first section headers and records.
Populate a second fetched results controller with your long fetch in a background thread.
You can either create a child context and use performBlock: or
use dispatch_async().
Assign the second FRC to the table view and call reloadData.
This worked quite well in one of my recent projects with > 200K records.
I know the answer #Mundi provided is accepted, but I've tried implementing it and ran into problems. Specifically the objects created by the second FRC will be based on the other thread's ManagedObjectContext. Since these objects are not thread safe and belong to their own MOC on the other thread, the solution I found was to fault the objects as they are being loaded. So in cellForRowAtIndexPath I added this line:
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
object = (TapCellar *)[self.managedObjectContext existingObjectWithID:[object objectID] error:nil];
Then you have an object for the correct thread you are in. One further caveat is that the changes you make to the objects won't be reflected in the background MOC so you'll have to reconcile them. What I did was make the background MOC a private queue MOC and the foreground one is a child of it like this:
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_privateManagedObjectContext];
}
Now when I make changes in the main thread, I can reconcile them easily by doing this:
if ([self.managedObjectContext hasChanges]) {
[self.managedObjectContext performBlockAndWait:^{
NSError *error = nil;
ZAssert([self.managedObjectContext save:&error], #"Error saving MOC: %#\n%#",
[error localizedDescription], [error userInfo]);
}];
}
I wait for it's return since I'm going to reload the table data at this point, but you can choose not to wait if you'd like. The process is pretty quick even for 30K+ records since usually only one or two are changed.
Hope this helps those who are stuck with this!
I am using CoreData in my application and it is a one-to-many relationship, I have two entities called folders and files. So a folder can have many files. So my first view controller shows all folders and when clicked on each folder, I show the files present in it.
Now all the folders and files present in it are created by user dynamically and are not coming from any database or from web server.
Now I have one more view controller, which I open from app delegate, and here I want to show some files.
I know that first we should pass the managedObjectContext, which will be declared in app delegate
ViewController *View = [[ViewController alloc]initWithNibName: #"ViewController" bundle:nil]
View.managedObjectContext = self.managedObjectContext;
So if we just pass managedObjectContext, can we access the file entity objects, OR we have to pass file entity object also.
Now as you know, I have two entities, called Folder and File, now to access files stored in any folder, I just do this.
NSMutableArray *filesArray = [self.folder.file mutableCopy];
But now, I want to show the files in anyView I want, which can be opened after two three navigations or it is opened directly from appDelegate.m.
To be very clear , my question is how to do this? i.e how can I get filesArray from AppDelegate.
Based on Below Answer edited. I have created a fetchResultsController this way
- (NSFetchedResultsController *)fetchedResultsController {
if (m_fetchResultsController != nil) {
return m_fetchResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"File" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"alarm" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"alarm!= nil"]];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return m_fetchResultsController;
}
and in ViewDidLoad and I written this way
- (void)viewDidLoad
{
[super viewDidLoad];
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
NSMutableArray *alarms = [[self.fetchResultsController fetchedObjects]mutableCopy];
NSLog(#"alarmsCount:%d",[alarms count]);
}
Regards
Ranjit.
If you share more details we could help you, but in the meantime...
If the master controller shows the folder and the details controller show the files, simply inject a reference of the selected folder to the detail controller.
For example, create a property in the detail controller like this (it is needed to be synthesized)
#property (nonatomic,strong) Folders* currentFolder;
When you create a detail controller, do the following
DetailController* detCtr = // alloc-init here
detCtr.currentFolder = // the folder you want to pass in
Some Notes
Use Camel Case notation.
Use NSFetchedResultsController for lazy loading data in association with tables, etc.
Rename entity Files and Folders in their singular form.
Edit
Ok, so if you want to access your app delegate from everywhere within the app, you can just call
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate someMethod];
Here, you can retrieve the folder you are interesting in with a public property declared in .h and synthesized in .m.
Now, this in not a so good approach to follow. AppDelegate class would remain as is.
Another, more elegant, approach is to create a singleton called SharedManager (or something else) that store the currentFolder that need to visualize.
Shared Manager will look like
//.h
#property (nonatomic,strong) Folder* currentFolder;
+ (SharedManager*)sharedInstance;
//.m
#synthesize currentFolder;
+ (SharedManager*)sharedInstance
{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[SharedManager alloc] init];
});
return sharedInstance;
}
That, after importing it, it will be used to set a folder as the current one
[[SharedManager sharedInstance] setCurrentFolder:theFolderYouWantToShare];
or to retrieve the current folder
Folder* theSharedFolder = [[SharedManager sharedInstance] currentFolder];
P.S. Do not abuse on Singletons. The singleton pattern I use requires iOS 4 and greater.
Edit 2
It was my fault. I did not understand the question.
If you need to retrieve all the files, you could just create a NSFetchRequest like the following:
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"File" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
where results will contain all the files in your store indipendently from any folder. You can use this array to display files in your table.
If you have a lot of files, I really suggest to take a look at NSFetchedResultsController class. It allows lazy loading together with UITableView.
You can find a good tutorial here: How To Use NSFetchedResultsController
Edit 3
How to go about reminders?
You should provide to your request (the one you use with NSFetchedResultsController) a predicate like the following
[request setPredicate:[NSPredicate predicateWithFormat:#"reminder != nil"]];
In this manner you retrieve the files where reminder is NOT nil. reminder is the property you are using in the model.
P.S. Check the code because I've written without Xcode support.