I have a UITableView that loads its array items fine on the first call. However when i refresh the tableview it crashes because the delegate method cellForRowAtIndexPath gets called to early. I have an integer which represents the index of my main data array and this gets reset to 0 in my refresh button. However its crashing because its trying to reload the data before its been reset. I would normally use indexPath.row as my index however the array is complicated and the indexPath.row will not match up to what i want to show for each cell. Heres some code, any help is appreciated.
This gets called when i pull down to refresh AND in viewDidLoad to prepare the data
- (IBAction)refresh:(id)sender {
itemIndexer = 0;
[sender beginRefreshing];
[self loadData];
}
Part of my loadData method
-(void) loadData {
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.Foo.myqueue", 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(backgroundQueue, ^{
[self downloadData];
dispatch_async(mainQueue, ^{
[self.tableView reloadData];
[_rcRefresh endRefreshing];
});
});
In viewDidLoad i call to initially load the tableview:
[self refresh:_rcRefresh];
I am getting a index outside bounds of array error. Which i have used breakpoints to determine why, the reason is simply because the refresh isn't getting called at all otherwise itemIndexer would be set to 0. Instead its a number 1 greater than the array size. If its really necessary i can post cellForRowAtIndexPath however I'm unsure if you need it if it works on the first call. So to summarise the first call to the tableview works and the data is loaded fine however the refresh causes the index to be outside the bounds of the array as if refresh: is never called.
For what you say, I can only try guessing:
Your numberOfSectionsInTableView/numberOfRowsInSection is returning a wrong number.
If [self downloadData] is asynchronous (you are making a server request and not waiting for the response), you should reloadData once you have the data.
The data you download is not merged properly with the data you already have.
Some more code (numberOfSectionsInTableView, numberOfRowsInSection, cellForRowAtIndexPath, downloadData) would definitely help.
Couple of points for clarity...
As indicated by #k20, it is not that tableView:cellForRowAtIndexPath: is called too early, but that you need to better manage your download data once your asynchronous process / method has completed.
tableView:cellForRowAtIndexPath: is a UITableView data source method, not a delegate method. It is worth mentioning this pedantic detail because it may help you or others better understand the code you are writing. ;)
The table view method calls are what they are - as I understand it, all table view methods are called in order, each time a UITableViewController is init or awakeFromNib. Those that you must override (tableView:numberOfRowsInSection: & tableView:cellForRowAtIndexPath:), and those that you choose to override will still execute in that same order.
Therefore a more appropriate title for your question might be... "How to update a UITableView with data from a download on an asynchronous thread."
Again #k20 is pointing you to the correct solution. Have you attempting placing these two lines of code...
[self.tableView reloadData];
[_rcRefresh endRefreshing];
within your async call, instead of back in the main queue?
It may be that your code as written is executing like this...
Prepare local variables for dispatch_q_t;
Commence download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
depending on time it takes, then finish download process;
Where you obviously would like to execute like this...
Prepare local variables for dispatch_q_t;
Commence download process;
depending on time it takes, finish download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
Try my suggestion and let me know how you go. Hope that helps.
Related
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.
I have UI that uses scrollview with paging as well UITableView in one view controller. In order to load UI I am using an array of NSDictionaries (sourceArray) which has necessary information to draw UI. This array is created by consuming RESTful API that returns an array of records. I am returning that array in completion handler. Later I add that array along with some other key:value pairs to create NSDictioary which is later added to sourceArray for UI drawing.
Now my question is how can I update my UI based on changes in sourceArray. I am calling [self.view layoutIfNeeded]; but it does not force redraw.
- (void)getStateDetails {
NSArray *statesArray = [self getStates];
for (NSString *state in statesArray) {
[self getStateDetailsInState:state withCompletionBlock:^(NSArray *records) {
NSLog(#"%#",records);
NSDictionary *dict = #{
#"kImage": state,
#"kName": state,
#"kRecord": records
};
[self.sourceArray addObject:dict];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view layoutIfNeeded];
});
}];
}
}
UI Details-
Viewcontroller contains UIScrollView with various UIViews which used dynamic data and a UITableView that contains dynamic data as well.
This is how I my current UI looks like.
The call to layoutIfNeeded will trigger your views to check to see if they need to update their layouts. Changing the contents of an array will not cause your view's layouts to need updating, so the call to layoutIfNeeded is unlikely to do anything.
In order for us to help you use the dictionary you load to update your UI, you're going to have to tell us what changes you expect the new array element to have on your UI.
One common thing to do is to display the contents of an array in a table view or collectionView. In that case you'd call the table/collection view's reloadData() function, which will cause it to ask it's data source for it's number of entries and re-render all of it's cells.
EDIT
Ok, and what is the relationship between the data you download and your UI? What code have you written that is supposed to cause the UI to update? You've changed your array, but you don't show any code that tells your view objects about updated data.
For a table view, that might be as simple as calling reloadData as mentioned above. For other views, how is your data being displayed into those views? What mechanism is supposed to install new data into those views?
The easiest way to get a view to re-draw itself is to call [UIView setNeedsDisplay] or by using [UIView setNeedsDisplayInRect]. This method tells the view that it needs to re-draw itself and it's subviews. However you shouldn't always have to call this. There are other methods that can cause views to redraw themselves. For instance if you add or remove a view to the view hierarchy using [UIView addSubview:] or [UIView removeFromSuperview].
These methods cause the system to call your view's drawInRect: method (assuming you overrode it with custom drawing code). If you did not override this method for custom drawing then it should cause your subviews to receive the same message so that they can get a chance to redraw.
Additional information here. Specifically read the section labeled "The View Drawing Cycle".
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
One of the stackoverflow posts where you can add an observer and force call the controller to relaod
How to reload UIViewController
You have to use [tableView reloadData] and then if needed
[self.view layoutSubviews];
[self.view setNeedsDisplay];
[self.view setNeedsLayout];
I'm getting some data from my server that I need to use as metadata for creating a table view. The problem is the data loads asynchronously with the flow of the app. I'm sure this is probably a simple fix, but how can I either pause the flow of the app until the data has loaded from the server, or update the table view as the data becomes available?
Thanks!
You can use an UIActivityIndicatorView to show the data is being downloaded. Once the data is downloaded in async mode you can reload UITableView.
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.center = CGPointMake(100, 100);
spinner.hidesWhenStopped = YES;
[self.view addSubview:spinner];
[spinner startAnimating];
Now call your method that downloads data from the network.
[self asyncDownloadMethod];
When you are notified i.e., by any delegate that the contents are downloaded, stop the indicator.
[spinner stopAnimating];
Hope this helps.
First, make sure that your data source methods are written correctly so that if there is no data yet, the table will indeed be empty. This is because the data source methods will be called when the table first appears, which is before you have data.
Then, when there is data, just call reloadData (on the main thread!). The same data source methods will run again, but this time there is data.
You haven't provided enough information to answer definitively, but the gist of it is you need to set up a delegate that will handle populating the table view once you've finished loading your data.
For example, the NSURLConnection class defines the connectionWithRequest:delegate: selector for loading data asynchronously. The delegate argument is defined as:
delegate
The delegate object for the connection. The connection calls methods on this delegate as the load progresses. Delegate methods are called on the same thread that called this method. For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
[tableView reloadData] does not call numberOfRowsInSection:(NSInteger)section
What i would write as an answer in stackoverflow if i saw a question like this:
Be sure that delegate and dataSource is connected perfectly.
Just in case check it if you are calling it from the main thread or not.
I tried my answers before asking the question.
I put the following line in my MyTableView class (extended from UITableView)
- (void)reloadData{
NSLog(#"MyTableView (reloadData) self.dataSource: %#, delegate: %#, isMainThread: %d",self.dataSource,self.delegate,[NSThread isMainThread]);
[super reloadData];
[self.delegate performSelector:#selector(tableReloaded:) withObject:self];
}
I see that it is called from mainthread and datasource and delegate is never nil.
I am not curious about why tableView does not call cellForRow as it first has to call numberOfRows to see if the result > 0. However, it doesn't call numberOfRows too...
Any ideas? What can be the reason for tableView to giveUp calling numberOfRows function?
Update:
I've put new lines to reloadData function to print FullStack to see if it is being called from tableView's own functions. The result: They are called outside of the tableView so it there shouldn't be any unexpected behaviour.
Update2:
"Sometimes": I have discovered this problem after my users started to say that "sometimes" they don't see the changes in the table. After that, i tried to hit run button in XCode continuouslly until the app opens with this bug. (30% percentage of my runs shows this bug). When it happens, until i restart the application, reloadData never works again.
Update3:
I put self.dataSource==myExpectedViewController and also [self.dataSource tableView:self numberOfRowsInSection:0] to see if even delegates are not nil maybe they were being cloned etc.. The result was True and numberOfRows were correctly returning>0. So delegates are correct and i have more items than zero.
Update4:
I tried it with a fresh UITableView (removing my custom UITableView), i got the same results.
Update5:
I've put a button on the screen which recreates the table and sets its delegates. Whenever the problem in the question happens, i hit this button and everything starts to work perfectly. So, there must be something that breaks internals of UITableView which invalidates every call to reloadData, but i still couldn't find it.
Edited : I tried to replicate the issue and found, when I presented another viewcontroller on current view controller which has this tableview. and then 1. tried to reload this tableview with some data and 2. dismissed the viewcontroller which is on top of my currentviewcontroller with animation, simultaneously , then since in this duration, table view is not visible so it won't be reloaded.
To resolve, you can put a delay there or use completion block of that animation to reload table with effect.
Otherwise problem can be related to thread, UIElements are expected to be updated only on main thread.
During API calls and other process, we enters in other thread, and mostly we ignore to check whether we are on main thread before updating UIElement.
You should call yourTable.reloadData() in main thread using DispatchQueue.main.async{yourTable.reloadData()} and make sure it is being called on main thread.
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.