core data fetch when switching between tabs - ios

I have an iOS app that uses Core Data as well as a tab bar controller. In the first tab, the user can add items that get saved in Core Data. In the second tab, there's other functionality that relies on read-only access to the Core Data store. When I open the app and switch between the tabs, the data looks the same, however, if I then add an item in the first tab, and switch to the second tab, it's not showing i.e. there hasn't been a refresh. In the second tab, I initially had the fetch done in viewDidLoad but I've moved it into viewDidAppear hoping that the fetch would happen everytime I switched to the second tab (and the corresponding view appeared) but I've still got the same problem.
How do I trigger a fetch/refresh when I click on the second tab after having added an item in the first tab interface?
-(void)viewDidAppear:(BOOL)animated
{
NSError *error = nil;
if (![self.fetchedResultsController 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();
}
}

You should listen for the notification NSManagedObjectContextObjectsDidChangeNotification
This notification tells you that there has been changes in your database.
For a good explanation look at this post:
How can I track/observe all changes within a subgraph?

Unless you manually change an aspect of the query, you don't need to manually performFetch: more than just once. If the results of the query would change, you'll get notified through the delegate methods. I'd just keep the call on viewDidLoad, as the view will be loaded lazily at the time the controller shows for the first time, which is, when you open it through the UITabBarController button
If you are manually changing the query in between presentations, give us more context so we can help with a proper implementation of it.
Edit: This is a bare minimum implementation that will reload a table view whenever the results of your query would change. The delegate methods allow for more specific behavior like inserting rows or deleting them without reloading the whole table, you can read enough information to implement this from the headers or in the apple docs but this will get you going.
- (void)viewDidLoad
{
[super viewDidLoad];
[self fetch];
}
- (void)fetch
{
if (!self.resultsController) {
//Optionally create resultsController lazily here, I didn't see where you created it
}
[self.resultsController performFetch:&error];
//The manual fetch call doesn't trigger delegate calls, so you refresh manually here after the fetch.
[self.tableView reloadData];
}
#pragma mark - UITableViewDataSource
//..
//Implement the table datasource to pull from the NSFetchedResultsController here.
//..
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView reloadData];
}

Related

UITableView reloadData crashes on reappearance in iOS 11

Update: In my view, the question is still relevant and so I am marking a potential design flaw that I had in my code. I was calling the asynchronous data population method in viewWillAppear: of VC1 which is NEVER a good place to populate data and to reload a table view unless everything is serialized in the main thread. There are always potential execution points in your code when you must reload you table view and viewWillAppear is not one of them. I was always reloading table view data source in VC1 viewWillAppear when returning from VC2. But an ideal design could have used an unwind segue from VC2 and repopulate the data source upon its preparation (prepareForSegue) right from VC2, only when it was actually required. Unfortunately, it seems like nobody had mentioned it so far :(
I think there are similar questions that have been asked previously. Unfortunately none of them essentially addressed the issue I'm facing.
My problem structure is very simple. I have two view controllers, say VC1 and VC2. In VC1 I show a list of some items in a UITableView, loaded from the database and in VC2 I show the details of the chosen item and let it be edited and saved. And when user returns to VC1 from VC2 I must repopulate the datasource and reload the table. Both VC1 and VC2 are embedded in a UINavigationController.
Sounds very trivial and indeed it is, till I do everything in the UI thread. The problem is loading the list in VC1 is somewhat time consuming. So I have to delegate the heavy-lifting of data loading task to some background worker thread and reload the table on main thread only when data load completes to give a smooth UI experience. So my initial construct was something similar to the following:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
});
});
}
This was very much functional until iOS10 from when UITableView stopped immediate rendering through reloadData and started to treat reloadData just as a registration request to reload the UITableView in some subsequent iteration of the run-loop. So I found that my app started to occasionally crash if [self.tableView reloadData] hadn't completed before a subsequent call to [self populateData] and that was very obvious since [self populateData] isn't thread-safe anymore and if datasource changes before the completion of reloadData it is very likely to crash the app. So I tried adding a semaphore to make [self populateData] thread-safe and I found that it was working great. My subsequent construct was something similar to the following:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(self.datasourceSyncSemaphore); //let the app know that it is free to repopulate datasource again
});
});
dispatch_semaphore_wait(self.datasourceSyncSemaphore, DISPATCH_TIME_FOREVER); //wait on a semaphore so that datasource repopulation is blocked until tableView reloading completes
});
}
Unfortunately, this construct also broke since iOS11 when I scroll down through UITableView in VC1, select an item that brings up VC2 and then come back to VC1. It again calls viewWillAppear: of VC1 that in turn tries to repopulate the datasource through [self populateData]. But the crashed stack-trace shows that the UITableView had already started to recreate its cells from scratch and calling tableView:cellForRowAtIndexPath: method for some reason, even before viewWillAppear:, where my datasource is being repopulated in background and it is in some inconsistent state. Eventually the application crashes. And most surprisingly this is happening only when I had selected a bottom row that was not on screen, initially. Following is the stack-trace during the crash:
I know everything would run fine if I call both the methods from the main thread, like this:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self populateData]; //populate datasource
[self.tableView reloadData]; //reload table view
}
But that is not something that is expected for a good user experience.
I feel the issue happens since UITableView is trying to fetch the offscreen top rows on reappearance, when scrolled down. But unfortunately after understanding so many damn things I could hardly sort it out.
I would really like the experts of this site to help me out of the situation or show me some way around. Thanks a loads in advance!
PS: self.application.commonWorkerQueue is serial dispatch queue running in the background in this context.
You should split your populateData function. Lets say for example into fetchDatabaseRows and populateDataWithRows. The fetchDatabaseRows should retrieve the rows into memory in its own thread and a new data structure. When the IO part is done, then you should call populateDataWithRows (and then reloadData) in the UI thread. populateDataWithRows should modify the collections used by the TableView.
UIKit runs on main thread. All UI updates must be on main thread only. There is no race condition if updates to data source happens on main thread only.
Important to understand is that you need to protect data. So if you are using semaphore or mutex or anything like this construct is always:
claim the resource for me. (ex: mutex.lock())
do the processing
unlock the resource (ex: mutex.unlock())
Thing is, that because UI thread is for UI and background thread is used for processing you can not lock shared data source, because you would lock UI thread as well. Main thread would wait for unlock from background thread. So this construct is big NO-NO. That means your populateData() function must create copy of data in the background while UI is using its own copy on main thread. When data are ready, just move the update into main thread (no need for semaphore or mutex)
dispatch_async(dispatch_get_main_queue(), ^{
//update datasource for table view here
//call reload data
});
Another thing:
viewWillAppear is not the place to do this update. Because you have navigation where you push your detail, you may do the swipe to dismiss, and in the midle just change your mind and stay in detail. However, vc1 viewWillAppear will be called. Apple should rename that method to "viewWillAppearMaybe" :). So right thing to do is to create a protocol, define method that will be called and use delegation to call the update function just once. This will not cause crash bug, but why to call update more than once? Also, why you are fetching all items, if only one has changed? I would update just 1 item.
One more:
You are probably creating reference cycle. Be careful when using self in blocks.
Your first example would be almost good if it looked like this:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
NSArray* newData = [self populateData]; //this creates new array, don't touch tableView data source here!
dispatch_async(dispatch_get_main_queue(), ^{
self.tableItems = newData; //replace old array with new array
[self.tableView reloadData]; //reload
});
});
}
(self.tableItems is NSArray, simple data source for tableView as an example of data source)
My assumption is that because you have referee cycle when accessing self.tableView inside getMain. Ans there is leaked versions of this table view somewhere in background which started to crash app in iOS 11
There is a chance that you can verify this with memory graph in Xcode.
To fix this access you need access weak copy of self like this
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
__weak typeof(self) weakSelf = self;
dispatch_async(self.application.commonWorkerQueue, ^{
if (!weakSelf) { return; }
[weakSelf populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData]; //reload table view
});
});
}
In iOS11, the proper way to do "the heavy-lifting of data loading task" is to implement the UITableViewDataSourcePrefetching protocol as described here: https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
If you properly implement 'tableView:prefetchRowsAtIndexPaths:', you don't have to worry about background threads, worker queues, temporary datasources, or thread synchronization. UIKit takes care of all of that for you.
Update: after looking at your question a bit more thoroughly, it seems like the root cause of your problem is using a mutable backing data structure for your tableview. The system expects that the data will never change without an explicit call to reloadData in the same run loop iteration as the data change. The rows have always been loaded lazily.
As other folks have said, the solution is to use a readwrite property with an immutable value. When the data processing completes, update the property and call reloadData, both on the main queue.

iOS - State restoration - Get previously restored view controller

I have a navigation controller with several view controllers inside.
While restoring app application(_:viewControllerWithRestorationIdentifierPath:coder:) method calls for each controller one by one initially for first in stack then for second and so on.
At time of restoring second view controller I need to get reference to first one (to make some connection between them).
Is there any way to get previously restored controller at this step without saving this controller somewhere in the app? (the same is about navigation controller I don't save referents to it anywhere in the app)
My understanding is that only the last View Controller viewed before the app was sent to background will have its state restored by the encode/decode methods you listed above, with NSCoder.
BUT you still need to save your own data, e.g. using NSKeyedArchiver or Core Data in case the app is terminated by user, or the device reboots. This may be the best solution in your case.
See link for encodeRestorableState here:
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621461-encoderestorablestate
Specifically it says:
This method is not a substitute for saving your app's data
structures persistently to disk. You should continue to save your
app's actual data to iCloud or the local file system using existing
techniques. This method is intended only for saving configuration
state or other information related to your app's user interface. You
should consider any data you write to the coder as purgeable and be
prepared for it to be unavailable during subsequent launches.
Encode the second view controller in the first view controller's encodeRestorableState method. e.g.
FirstViewController.m
- (void)prepareForSegue:(UIStoryboardSegue *)segue{
// configure and store it...
// self.secondViewController = controller;
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
if (self.secondViewController) {
[coder encodeObject:self.secondViewController forKey:kSecondViewControllerKey];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
self.secondViewController = [coder decodeObjectForKey:kSecondViewControllerKey];
}
- (void)applicationFinishedRestoringState{
// self.secondViewController's properties are now also decoded and ready to be used
}
application:viewControllerWithRestorationIdentifierPath: can return nil for the second view controller and it will one from the storyboard for you and the reference will be what is returned in decodeObjectForKey.

Wait saveInBackground Parse before viewDisappear

I would like to wait saveInBackground Parse method before the view disappears. Because the view following is using this data, but that doesn't have the time to refresh...
Here is my code :
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
PFQuery *urlImage = [PFQuery queryWithClassName:#"urlImage"];
[urlImage whereKey:#"objectId" equalTo:#"IcK6mFChL7"];
[urlImage getFirstObjectInBackgroundWithBlock:^(PFObject *urlImageParse, NSError *error) {
if (!error) {
[urlImageParse setObject:self.photoURL.text forKey:#"URL"];
[urlImageParse saveInBackground];
} else {
NSLog(#"Error: %#", error);
}}];
}
Can I make this code in other place that viewWillDisappear:? Or maybe use MBProgressHUD?
viewWillDisappear is a place to execute code, knowing that the view is about to disappear. I believe what you are looking to do is run some action, prior to the view actually going away.
This part right here -> [urlImage getFirstObjectInBackgroundWithBlock:^
means that the code inside that block is going to run on a different thread. So what you are currently saying is, when the view is about to disappear, spin off this other thread and save something in the background, but continue on doing whatever you need to do, like make this view disappear. That's what you're telling the system. So it continues on, doing what it was going to do, not caring about the results of saving that object.
There is a reason that your view is about to disappear. Something happened in your app where the system is thinking it needs to close this view and present another one. It could be that you hit a back button, it could be that you clicked a save button, and at the end of that code, you are asking the system to pop this view off the stack. We really don't know with only the code you posted. What we can assume though, is whatever action was taken to make this view disappear, is where you should be trying to save this object, and you should probably be waiting for a response before you leave this view. If the next view is dependent on that information, then it doesn't make sense to dismiss this view, and present the next one, until you get a successful response that this object was saved. This is just an assumption though, since I don't understand what your app is doing or what is going on when you are trying to save this data, and what the next view looks like. I would say that typically, if you need this stuff to be saved, in order to continue on in your app, you should be saving this data and waiting for a response, to either display an error message to the user, or if successful, then move on to the next screen.

How to handle concurrency by queue/NSThread for UI design, iOS dev

What I am trying to achieve is simple, from first thinking though. I found it hard to handle at last.
I would like to push a table view as a selection list, user select one cell and the cell string was sent to the previous view as selected string, simple huh??
See two pictures first
what bothers me is that:
I would like to provide (at least) two buttons, one on the left is back button auto-generated by navigation controller, and the right one is for editing. And the navigation controller is defaulted to have two buttons (from my knowledge). So there is no place for "Done" button, which is supposed for user to tap and then confirm and pop to the previous view.
So, when the user tap a cell, "Wearing" for example, I would like the following to happen, automatically and visually SEEable for user:
user can SEE that "Housing" cell is unmarked
then user can SEE that "Wearing" cell is marked
then after a little time gap (say 0.2 second), pop to the previous view and update the selection, automatically.
At first I thought it's easy but it's definitely not. Here is my code for doing it, but working wired
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul);
dispatch_async(queue, ^{
//unmark previous cell
if (selectedIndexPath!=nil) {
[[self.tableView cellForRowAtIndexPath:selectedIndexPath]setAccessoryType:UITableViewCellAccessoryNone];
}
selectedIndexPath=indexPath;
//get the selected cell to mark
UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
dispatch_sync(dispatch_get_main_queue(), ^{
//wait a little
[NSThread sleepForTimeInterval:0.2];
//return to previous view
NSLog(#"here.........");
if ([objectToUpdateCategory respondsToSelector:#selector(updateCategoryTo:withSelectedIndexPath:)]) {
NSLog(#"sending.......... update info");
[objectToUpdateCategory updateCategoryTo:cell.textLabel.text withSelectedIndexPath:selectedIndexPath];
NSLog(#"sent update info");
}
[self.navigationController popViewControllerAnimated:YES];
});
});
The tricky thing is that if I put [self.navigationController popViewControllerAnimated:YES]; to the last, the view will not visually update the unmark and mark step and go back to the previous view immediately. At first, when I didn't consider the unmark thing, the “queue" stuff in code can do the mark step visually before popping back, but sometimes not working. I don't know if my code is correct, actually I don't quite understand this queue tech from apple. But I'm pretty sure it has something to do with NSThread / queue or else that handle concurrency. I've checking Apple documents for a whole day and found no direct answer.
Hope someone could help me on this, thanks in advance :)
To "after a little time gap (say 0.2 second), pop to the previous view", use the performSelector:withObject:afterDelay: methods or one of its variants, e.g.:
[self performSelector:#selector(delayedPop) withObject:nil afterDelay:0.2];
and put the popViewControllerAnimated in the delayedPop method, e.g.:
-(void)delayedPop{
[self.navigationController popViewControllerAnimated:YES];
}
First of all, as I wrote in my comment, you shouldn't update the UI on a background thread. This will cause a lot of problems, including the UI not being updated immediately. In your case you don't need to use dispatch_async or dispatch_sync at all. What I would do is create a property in the view controller that displays the categories table view:
#property (nonatomic, weak) id<CategoryControllerDelegate> delegate;
When you push the category controller on the stack you set your expense controller as the delegate. Then, when the user makes a selection in the category controller, you call a method on the delegate (defined in a protocol), for example the one in your code sample:
#protocol CategoryControllerDelegate<NSObject>
#optional
- (void) updateCategoryTo: (NSString*) category withSelectedIndexPath: (NSIndexPath*) path;
#end
After that you pop the current view controller off the stack.

Saving NSManagedObjectContext casuing UITableView cells to disappear

I'm having the following issue.
I'm writing a RSS reader using CoreData and Apple Recipes example as a guide. I have a refresh button that re-downloads the RSS and verify using NSFetchRequest if there is new data. Once I finish going over the elements I commit the changes via the NSManagedObjectContext save method.
After the context save, the tableView disappears!
I then decided to call reloadData on the tableView to reflect the changes. So, once the NSManagedObjectContext is saved I call:
[self performSelectorOnMainThread:#selector(updateTableItems) withObject:nil waitUntilDone:NO];
-(void) updateTableItems {
[self.tableView reloadData];
}
This action causes the cell to delete the data while scrolling, when I pop the view and go back, I see all the changes and everything is okay.
I also read in one of the threads that I should use NSFetchedResultsController and make sure the UITableView is the delegate, same issue as the previous one.
What am I doing wrong ?
Why can't I see the changes in place?
Why the cell's content is being deleted?
Thanks!
It sounds like you are using two or more context on separate threads. You commit the save on the background thread context but don't merge the changes with the context connected to the UI on the front thread. This causes the UI context to come out of sync with the store which cause table rows to disappear when you scroll.
When you pop the controller by leaving the view, the context is deallocated such that when you go back to the view a second time, you have a new context aware of the changes to the store.
To prevent this problem, call refreshObject:mergeChanges: on the front context immediately after you save the background context. Have the front context register for a NSManagedObjectContextDidSaveNotification from the background context
I have been having a similar issue and it was driving me crazy. Basically in my code there are loads of competing threads trying to update the same data at the same time (the data behind the table view) and I think this some how causes the UITableView to "blow up" and its delegate methods stop firing. (You can prove this by adding NSLog's into the delegate methods).
It happens totally randomly and is really difficult to replicate.
I tried all sorts to fix this but the only thing that seems to reliably ensure that it can't happen was completely recreating my UITableView everytime the data changed as below. (So basically change everywhere where you call [self.tableView reloadData] with the following)
// The UITableView may in rare circumstances all of a sudden failed to render
// correctly. We're not entirely sure why this happens but its something to
// do with multiple threads executing and updating the data behind the view
// which somehow causes the events to stop firing. Resetting the delegate and
// dataSource to self isn't enough to fix things however so we have to
// completely recreate the UITableView and replace the existing one.
UITableView* tempTableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 387)] autorelease];
tempTableView.separatorColor = [UIColor grayColor];
tempTableView.backgroundColor = [UIColor clearColor];
if (self.tableView != nil)
{
[tempTableView setContentOffset:self.tableView.contentOffset animated:NO];
}
tempTableView.delegate = self;
tempTableView.dataSource = self;
[tempTableView reloadData];
if (self.tableView != nil) {
[self.tableView removeFromSuperview];
self.tableView = nil;
}
[self.view addSubview:tempTableView];
self.tableView = tempTableView;
I know this isn't the ideal fix and doesn't really explain the issue but I think this is an iOS bug. Would welcome any other input however.

Resources