This question already has answers here:
How can I tell when UITableView has completed ReloadData?
(18 answers)
Closed 5 years ago.
Calling tableView.reloadData() seems to initiate the reload on a different thread. If I immediately try to select a cell I've added to my data structure, it may not have been reloaded by the TableView yet. Is there a way to tell when the TableView has completed the reloadData() process. My actual code is:
DispatchQueue.main.async {
self.tableView.reloadData()
self.selectNode(node: newNode)
}
Where selectNode is my function to locate the TableViewCell displaying that node. Frequently, it fails because TableView hasn't re-displayed it yet. I can "brute force" a solution by inserting a wait loop that checks to see if TableView has requested all my rows since the reload, but I had hoped there was a more elegant solution. I'm also uncomfortable with wait loops in general.
I wanted to share the solution I found to the problem, as well as what it implies about how tableView.reloadData works. First, here's the code that I finally arrived at that works:
func addNewItem() {
// here we create the kind of item the user chose
//ending with this
tableView.reloadData()
queueCheckForDone()
}
func queueCheckForDone() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(1), execute: {
self.checkForDone()
})
}
func checkForDone() {
if items[items.count-1].viewCell != nil { // has last view cell been created
self.selectItem(item: addedItem)
} else {
queueCheckForDone()
}
}
The issue was that immediately after the reloadData, I wished to select the cell that I had just been added. But, it wasn't there immediately after the call, because the tableView hadn't finished loading. reloadData wants to run on the main thread, and my code, an event handler, was blocking it by being on the main thread. As soon as I relinquished the thread to wait, for even a millisecond, reloadData seized the thread and reloaded the whole table(> 250ms). When I finally got control again, the cell I was looking for was always there, and so I never executed the else clause of the checkForDone test.
Related
I have a UITableView in my ViewController. To populate it, I have to make an async request that may take up to a second to complete. Where should I put it?
When I tried to make ViewDidLoad async and make a call from there, ViewWillLayoutSubviews throws an error, because it tries to position some elements that weren't assigned yet - due to the fact that not all code from ViewDidLoad was executed yet (I think).
Before awaiting anything in ViewDidLoad you need to setup all your view logic. Otherwise your view initialization will not be finished when ViewDidLoad method returns. That could be a potential cause for ViewWillLayoutSubviews to fail. If it still fails, use a try/catch to make sure your service is working:
public override async void ViewDidLoad()
{
base.ViewDidLoad();
// setup all the view elements here (before the async call)
try
{
var results = await MakeYourAsyncRequest();
InvokeOnMainThread(() =>
{
_tableView.Source = ...; // do something with the results
_tableView.ReloadData();
});
}
catch(Exception ex)
{
// do something with the exception
}
}
Try putting the tableView.ReloadData(); method inside
dispatch_async(dispatch_get_main_queue(), ^{} this might solve your issue.
-:As a general rule, you should try to make sure that all of your UI interaction happens on the main thread. And your data fetching task will work in background. It looked like you were calling reload Data from your
background thread, which seems risky.
Depending on the data I would put the call in the AppDelegate. When the app launches the data should be fetched and saved.
When your UITableview appears it will already have the data ready or maybe an error message since you already know the result of the fetch.
The data may change thats why I would also put the fetch call in viewWillAppear() of your ViewController with the UITableview.
ViewDidLoad() is a method that gets called only once. Also it is called as the first method of the ViewController lifecycle.
It would be good if you read a bit about it VC lifecycle.
You can help yourself by trying it in code with printf("method name").
I see questions regarding long delays in displaying UIImageViews after downloading, but my question involves long delays when
reading from local storage.
After archiving my hierarchy of UIImageViews to a local file (as per narohi's answer in
How to output a view hierarchy & contents to file? ),
I find that if I want to reload them, it takes 5 to 20 seconds for the views to actually appear on screen,
despite my setting setNeedsDiplay() on the main view and all the subviews.
I can immediately query the data contained in the
custom subclasses of UIView that get loaded -- showing that NSKeyedUnarchiver and all the NS-decoding and all the init()'s have completed -- however
the images just don't appear on the screen for a long time. Surely the next redraw cycle is shorter than 5-20 seconds...?
It seems odd that images from PhotoLibrary appear instantly, but anything loaded from local file storage using NSKeyedUnarchiver takes "forever."
What's going on here, and how can I speed this up?
.
.
To be explicit, the relevant part of my Swift code looks like this:
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
myMainView.addSubview(view)
view.setNeedsDisplay()
// now do things with the data in view ...which all works fine
I find that, even if I add something like...
for subview in view.subviews {
subview.setNeedsDisplay()
}
...it doesn't speed up the operations.
We are not talking huge datasets either, it could be just a single imageview that's being reloaded.
Now, I do also notice these delays occurring when downloading from the internet using a downloader like the one shown in
https://stackoverflow.com/a/28221670/4259243
...but I have the downloader print a completion message after not only the download but when the (synchronous operation)
data.writeToFile() is complete (and before I try to load it using NSKeyedUnarchiver), so this indicates that the delay
in UIImageView redraws is NOT because the download is still commencing....and like I say, you can query the properties of the data and it's all in memory, just not displaying on the screen.
UPDATE: As per comments, I have enclosed the needsDisplay code in dispatch_async as per Leo Dabus's advice, and done some Time Profiling as per Paulw11's. Link to Time Profiling results is here: https://i.imgur.com/sa5qfRM.png I stopped the profiling immediately after the image appeared on the screen at around 1:00, but it was actually 'loaded' during the bump around 20s. During that period it seems like nothing's happening...? The code is literally just waiting around for a while?
Just to be clear how I'm implementing the dispatch_async, see here:
func addViewToMainView(path: String) {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
...Since posting this I've found a few posts where people are complaining about how slow NSKeyedUnarchiver is. Could it just be that? If so, :-(.
SECOND UPDATE: Ahh, the "let view = " needs to be in the dispatch_async. In fact, if you just put the whole thing in the dispatch_async, it works beautifully! so...
func addViewToMainView(path: String) {
dispatch_async(dispatch_get_main_queue(), {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
This works instantly. Wow.. Credit to Leo Dabus. Leaving this here for others...
I am developing an iOS application in Swift.
I display data from database inside a collectionview but it takes too much time to appear on screen.
Surely a thread problem because on Xcode console, I can see data being displayed 20 seconds before it appears on the screen. Here is my code :
https://github.com/ProjetCedibo/projet/blob/master/Projet-L3-master/appli/SJEPG/SJEPG/CenterViewController.swift
How can I make the collectionView being displayed faster ?
If this happens, it means you are making UI changes from a thread other than the main thread. Try this:
func refresh() {
dispatch_async(dispatch_get_main_queue(), {
self.eventsCollection.reloadData()
})
}
What happens is that code is probably run on a secondary thread. Any UI changes you make should be made on the main thread. So try this:
dispatch_async(dispatch_get_main_queue()){
// reload your collection view here
})
In addition to #StefanArentz answer
For Swift 3:
DispatchQueue.main.async {
// code to reload collection view
}
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.
[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.