NSFetchedResultsController (and UITableView) delegate methods calls growing and growing - ios

Setup: UITableView with FRC. Rows are simple list of text content user can pull to refresh to get the latest.
I’m seeing strange behavior where cellForRow is called for each row, multiple times. So I see it for 0,0 0,1 0,2 0,3 (visible rows), but these 4 rows all have cellForRow called multiple times. But the first time you view the list they're called once. The second time, twice, etc. By the 7th time, after the user sees the content, behind the scenes it continues to try and configure the cell over and over and eventually crashes.
So if you go to any list of content, it hits the server, downloads the stories, creates NSMOs and displays. In the logs, I see configureCell called once for each visible row. If I refresh, I see the same. BUT if i navigate to a different screen, then come back, when I pull to refresh I notice that cellforrow is called twice for each row. If I continue this process of leaving and coming back, every time I do, cellforrow is called an additional time. Logging some of the fetched results controller delegate methods, I see willchangecontent before each set of cellforrow calls. Can someone help me determine why my cellforrow method is called a growing number of times?
One idea was the way I was setting up FRC. I followed code like CoreDataBooks and moved things to viewdidload, but still seeing issue.
I have a property in the .h and in the .m have what i thought was a standard setup:
- (NSFetchedResultsController *)fetchedResultsController
{
//NSLog(#"fetchedresulscontroller");
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
// initialize fetch request. setup predicate, sort, etc.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"date" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
// perform actual fetch. delegate methods take it from here
NSError *fetchError = nil;
if (![self.fetchedResultsController performFetch:&fetchError])
{
// 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 %#, %#", fetchError, [fetchError userInfo]);
abort();
}
return _fetchedResultsController;
}

andrewbuilder was on the right track. It all had to do with the FRC, but the trick was the third party SWReveal library used for the menu. Turns out, I was creating a new VC each time (previous wasn't deallocated) and the FRC was looking at all live view controllers. So each time i tapped a selection from the menu, another was added and the config calls were called for that.
The solution is to nil out the FRC delegate in viewwilldisappear and set it in viewwillappear

Related

Table not updated after addition

I want my app to check the core data store at start-up. If the store is empty, it will add two items into it. What is the best way to implement this?
Can I put the following code in viewDidLoad?
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MonitorItem"];
self.monitorItemArray = [[managedObjectContext
executeFetchRequest:fetchRequest
error:nil] mutableCopy];
// If the core data is empty, populate it with the two compulsory items
if ([self.monitorItemArray count] == 0)
{
self.AddMandatoryItems;
}
[self.tableView reloadData];
}
I have searched other articles but none seems to give me an answer that I could understand.
Getting information from managedObjectContext in viewDidLoad is good.
If in cellForRowAtIndexPath you populate the cells from self.monitorItemArray than there is no reason to call reloadData (Which essentially erase the entire table view and re-draw it from scratch - which is exactly what happens when the view is appearing on screen any way...).
If you also show information from a web service, you can call reloadData in the response method to replace the existing data with the one that came from the web. Otherwise - if only information from core data is shown on the table view - no need for reloadData (Or maybe only in a case where the information in your managedObjectContext has changed while the table view is on screen).

UICollectionView refresh data

I am using a CollectionView which displays an array of objects.
On clicking a button i fill this array with a new set of data.
I want to refresh the CollectionView with this data.
Is there any statement to update this instead of comparing each items and selectively deleting and adding? The reloadData usually ends up in the following error.
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
In Short, I am looking for the following steps...
1)Fill the datasource array, show the data.
2)Fill the datasource array with new data, refresh the CollectionView to show the data.
Thanks in Advance
Try - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion.
In your case, you want "an all new set of data", so to speak, so e.g:
[myCV performBatchUpdates:^{
// one of:
// a)
[myCV deleteSection:someIndexSetForTheEntireSection];
[myRealDataSource empty:someIndexSetForTheEntireSection];
//
// OR b)
[myCV deleteItemsAtIndexPaths:someSetOfIndexPaths];
[myRealDataSource removeIndexPaths:someSetOfIndexPaths];
// Either case:
NSArray *indexPaths = [myRealDataSource getNewDataAndReturnIndexPaths];
// if a)
[myCV insertSections:newIndexSetForNewSection];
// Either case:
[myCV insertItemsAtIndexPaths:newIndexSetForInsertions];
}
completion:^(BOOL finished) {
NSLog(#"Done.");
// Maybe signal something else if you want.
}];
performBatchUpdates:completion: will expect the deletions & insertions from the original data source check entering the function to add up to the correct data source size leaving the method. It will loudly complain otherwise.
If you only have one section (section 0), you can be much more general than specific index paths if you are always "removing everything" and "inserting a complete new set".
Another option to to use KVO to listen on insertions and removals from the data source and simply reloadData, reloadItemsAtIndexPaths: or reloadSections: as appropriate.
I prefer the reactive KVO version, as I tend to use collection views with Core Data, and not pseudo-static or in-memory NSArray's.
To figure out the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION issue, I'd setup a breakpoint on all exceptions, and try to discover what is really triggering the issue. Likely your datasource is gone and there's a bad access when you try to read/write from it.
Suppose you arrive on your view then you can add data to your array in viewDidLoad method like so:
-(void)viewDidLoad
{
[super viewDidLoad];
// If you have already data
self.arr_sample=(NSArray *)array;
/*
When you want to download data from server than you have to call reloadData
method of collection because all delegate method already called before view load.
So loading data will take time to load data than you have to call all delegate
method of collectionview by calling reloadData method.
*/
[self loadData];
// Do any additional setup after loading the view from its nib.
}
but first of all you have set the delegate of collectionview .
do you want to download data from server than you can call reloaddata method of collection view. such as
-(void)loadData
{
// Do your downloading code her and call to reload data method of collectionview
[collectionview reloadData];
}
now again do you want to refill your array with new data on your button click than you can do
-(void)refillData
{
// Download new data code here and follow
[array removeAllObjects];
array=(NSArray *)newarray;
[collectionview reloadData];
}

I need my UITableViewController to reflect database changes, please help me decide the best approach and show me how

Here is how things are looking:
My root controller is a UITabBarController. Number 13 in the image is my default view that I've set to show a list of people when the app is first opened up.
Number 4 shows a page I'm taken to by clicking a settings link on page 3. On page 4 I can manage the people on the main view by confirming pending people or taking confirmed people offline. Basically I can set whether they show up or not or delete if I wish.
Changes are always shown in 5 and 6 because I remove rows after after deletions, confirmations or status changes. When making edits in the detail views 7 the changes are updated easily because I update the Person object that has been passed over during segue. So when I go back to the table view in 5 I just "self.tableView reloadData" in my viewWillLoad method.
In table 6 and action sheet pops up when I tap on a row and gives me the option to take any currently confirmed person back offline. All I do is remove the row and that's it. ViewDidLoad is run when ever I go back to controller view 4 then click "pending" or "confirmed".
Now the issue
Unlike my controller in number 4 my main table in number 13 is shown when my first UITableViewController tab is tapped or when the app is first loaded up. My code below is loaded in the viewDidLoad method and therefore only loaded once.
This is how my data is shown on main view:
- (void)populatePeopleArrayWithCloudData {
// Grab data and store in Person instances and then store each person in array
people = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"People"];
[query whereKey:#"active" equalTo:#1];
[query orderByDescending:#"createdAt"];
[query setLimit:10];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
Person *person = [[Person alloc] init];
[person setName:[object objectForKey:#"name"]];
[person setNotes:[object objectForKey:#"notes"]];
[person setAge:[[object objectForKey:#"age"] intValue]];
[person setSince:[object objectForKey:#"since"]];
[person setFrom:[object objectForKey:#"from"]];
[person setReferenceNumber:[object objectForKey:#"referenceNumber"]];
PFFile *userImageFile = object[#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:imageData];
[person setImage:image];
}
}];
[person setActive:[[object objectForKey:#"active"] intValue]];
[person setObjectId:[object objectId]];
[people addObject:person];
}
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
NSLog(#"Calling reloadData on %# in viewDidLoad", self.tableView);
[self.tableView reloadData];
}];
}
The code grabs my data from parse, runs a for loop storing each person in a Person instance and adding it to an NSMutableArray instance "people". This people instance is accessed by my tableview datasource methods.
When I tap another tab and then tap my main tab again, viewDidLoad is not loaded again. Ok, that's understandable. Now what if I want my main table(13) to reflect changes made in tables 5 and 6? I thought I could just go to viewWillAppear and run "self.tableView reloadData". I did this and nothing happened. Well that's what I thought. After some messing around I realised that my "people" array wasn't updated with the latest data from my database.
I then decided to try and call my populatePeopleArrayWithCloudData method from viewWillAppear but this just causes duplicate entries. My people array is used to populate table 13 twice. I said ok let me take the populatePeopleArrayWithCloudData method out of viewDidLoad. This works and changes are shown but then I start having issues with images being loaded, showing up late , last row issues etc. Calling populatePeopleArrayWithCloudData method in viewDidLoad solves all those issues.
I like having the code in viewDidLoad because it means less calls to the database. Like when a user clicks on a row and it takes them to the detail views 14 and 15 and return back to 13 no calls are made to the database like they are when code is in viewWillAppear.
So I'm wondering if the best way to solve this issue I'm having to, is to some how update the "people" instance from tableView 6 (as soon as "take offline is click") and in tableview 5 (as soon as "publish" is clicked to confirm a person). Publishing a user sets active status to 1. Objects with a status of 1 are included in the query result above and therefore shown in the main tableview list. Taking a person offline sets their active status to 0 meaning they don't show up in the main list.
1. Would it be good/right to access the "people" instance from my table views 5 and 6 then update that people array appropriately? Maybe after the update have a method similar to populatePeopleArrayWithCloudData that repopulates people. Then in viewWillAppear I just reload the table again hoping it detects the new people instance and uses that.
2. If so how do I do this? Can you show a clear example if possible...
3. If not what way would you suggest I do this and can you show a clear example.
Thanks for your time
Kind regards.
There are several approaches you can take for this but (assuming I understand your problem) I think that the best is to have a shared instance of your array of people. What you do is create an NSObject called ManyPeople and add the following:
ManyPeople.h:
#property (nonatomic, strong) NSMutableArray *manyPeople;
+(ManyPeople *) sharedInstance;
ManyPeople.m
+ (ManyPeople *) sharedInstance
{
if (!_sharedInstance)
{
_sharedInstance = [[ManyPeople alloc] init];
}
return _sharedInstance;
}
Now all your UIViewControllers can call ManyPeople *people = [ManyPeople sharedInstance]; and will get the same array. When make a call to the database you populate the shared instance. When you make changes to the settings you also make changes to the shared instance. The beauty is that when you return to your view controller 13 all you do is reloadData and you don't need to make a call to the database. The data source is already points to the updated array.

refreshing an nsfetchedresultscontroller not tied to a uitableview

i am wrting an app that uses a number of NSFetchedResultsControllers (FRCs), each for a different Managed Object subclass in my data model. several of these FRCs are part of UITableViewControllers, but several are not. i have two questions: one, why is one of my FRCs not updating when i add a new object to the MOC, and two, should i be using an FRC at all for this purpose? i thought it would save me from having to put fetch requests in all over the code, but perhaps it only makes sense to use an FRC with a tableview or other such object.
so, here are the details.
one of the FRCs not tied to a tableview is one that keeps track of schedules. the SetScheduleViewController has the following properties (among others), which are passed in via the parent view controller:
#property (weak, nonatomic) NSFetchedResultsController *scheduleFetchedResultsController;
#property (weak, nonatomic) NSManagedObjectContext *managedObjectContext;
this FRC was created by another object (which maintains a strong pointer to it) via the following code
- (NSFetchedResultsController *)scheduleFetchedResultsController {
if (_scheduleFetchedResultsController != nil) {
return _scheduleFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Schedule" 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:#"start" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *fRC = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
NSError *error = nil;
if (![fRC performFetch:&error]) {
IFLErrorHandler *errorHandler;
[errorHandler reportErrorToUser:error];
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
self.scheduleFetchedResultsController = fRC;
return self.scheduleFetchedResultsController;
}
(an aside: the reason the cacheName is set to nil is that i seem to be able to assign a cache name to only one of the many FRCs in this app, or the app crashes...if only one FRC is given a cache name and the rest have cache names set to nil, all seems well, though i am concerned that without a cache the performance may be terrible as the size of the persistent store grows...but that's another matter)
within the SetScheduleViewController, a schedule can be created or deleted, and on loading the SetScheduleViewController the most recently created schedule is loaded. when a schedule is created a new Schedule Managed Object is created, and then the MOC is saved as follows:
Schedule *newSchedule= [NSEntityDescription insertNewObjectForEntityForName:#"Schedule" inManagedObjectContext:self.managedObjectContext];
newSchedule.start=newStartTime;
//etc
NSError *saveError;
if (![self.managedObjectContext save:&saveError]) {
// 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 %#, %#", saveError, [saveError userInfo]);
abort();
this successfully saves the new MO, but the FRC does not update...i've checked this several ways...for example, if after saving a new schedule i re-enter the SetScheduleViewController and check [[self.scheduleFetchedResultsController fetchedObjects] count] it is not incremented. however, if i quit the app and open it again, lo and behold, the FRC fetched object count is incremented and the new object is indeed there.
i should note that the scheduleFetchedResultsController does not have a delegate assigned, unlike the FRC's attached to tableviewcontrollers, which are all working fine. i didn't assign a delegate because from what i could tell, the FRC delegate methods in the tableviewcontrollers only deal with updating the tableview when the FRC changes content...i do not see any delegate methods that refresh the FRC itself when a new object is added and the MOC saved.
so, my two questions again are: 1) why is the FRC not refreshing (and how can i make it refresh), 2) does it even make sense to use an FRC to manage fetched results for a managed object not tied to a tableview, and should i instead simply perform a fresh fetch from the MOC every time i need access to the list of objects?
any help much appreciated.
In the NSFetchedResultsController documentation it is stated that the FRC is in "no-tracking" mode if no delegate has been set. Also, the delegate must implement at least one of the change tracking delegate methods in order for change tracking to be enabled.
The delegate does not have to be a table view controller, so you could use your
SetScheduleViewController as a delegate and just implement the controllerDidChangeContent: delegate method. Inside that method, the updated
fetchedObjects is available, and you can e.g. update any UI elements in the view
controller.
Update: Passing the FRC from the parentVC does not make much sense. Each view controller should have its own FRC. So scheduleFetchedResultsController should be a method in the childVC. And as the FRC is created "lazily" in the getter method, the getter has to be called somewhere.
In the case of table view controllers, this happens because all table view data source methods
access self.fetchedResultsController.
If your childVC does not access self.fetchedResultsController then the FRC
will not be created. That could be the reason why calling [self.fetchedResultsController performFetch:&error] in viewDidLoad, as suggested in the other answer, solved your problem.
The delegate method controllerDidChangeContent: will then be called if the result
set changes during the lifetime of the childVC. That's where using an FRC makes sense.
If you just want to fetch the current set of objects when the childVC is loaded then
you don't need a FRC, and you can just execute a simple fetch, e.g. in viewDidLoad.
I've faced the same problem before. The reason is you didn't performed the fetch of FRC.
Add the following code on -viewDidLoad:
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}

Reload UITableView data every time context changes?

I have a UITableViewController that's a subclass of CoreDataTableViewController (it's the Stanford class). That implements a fetchedResultsController.
Now, in my viewWillAppear, I have this:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.managedObjectContext) {
[self useManagedDocument];
}
}
It initializes the managedObjectContext if I don't have one, and gets it from a helper class. In the MOC setter, I initialize the fetchedResultsController:
- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
_managedObjectContext = managedObjectContext;
if(self.managedObjectContext) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:CD_ITEM_ENTITY_NAME];
request.sortDescriptors = #[[NSSortDescriptor
sortDescriptorWithKey:CD_ITEM_NAME_PROPERTY
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
request.predicate = nil;
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
} else {
self.fetchedResultsController = nil;
}
}
When my program starts, it loads the table data up correctly and my debugger says there was a fetch request made. However, after inserting data into my Core Data graph, and saving, it says the context changes and fires this delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
/*NSError *error;
[controller performFetch:&error];
if(error) {
NSLog(#"%#", error);
}
[self.tableView reloadData];*/
}
I commented this because it wasn't working. Basically, what I want to do is reload the data every time the context changes. If I add an item in another view controller and then go back to this one, it should reload in that case too.
How do I implement this? I tried doing performFetch in that delegate method and it entered it (I checked by setting a breakpoint inside), but the performFetch did nothing and my table wasn't reloaded.
When I add an item in a modal VC (another one I have for managing items) this is what happens in my logger:
2013-05-10 22:41:38.264 App1[7742:c07] [ItemCDTVC performFetch] fetching all Item (i.e., no predicate)
2013-05-10 22:41:46.454 App1[7742:c07] NSManagedObjects did change.
2013-05-10 22:41:46.456 App1[7742:c07] NSManagedContext did save.
When I close my app but do not quit it from the multitasking bar, and then reopen it, it does nothing. No fetch. Well, if the context didn't change I don't want it to fire a request, but imagine if the user adds an item in another ViewController and then goes back to my ItemCDTVC, which lists all items. Does it get a context changed notification so it can call the delegate method to update the table, or will I always have to refresh regardless of changes in my viewWillAppear? I currently have it set to do it only once, on app load.
Fixed, all I had to do is put a one liner in that delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
This ends updates, inserts, deletes, and changes made to the table (refreshing my view, essentially) the same as per Apple's documentation.
It now updates on content change.

Resources