In my iOS5 application, I have UITableView that would display cells based on NSString values stored in progressLabelArray. I have registered a call back from my lower layers and I receive them in a C function from where I manage to call the UI update method through a reference to self.
static void callback_handler(int nCode) {
[refToSelf updateProgressView:nCode];
}
-(void) updateProgressView:(int32_t) nCode
{
NSString *status = nil;
status = [self progressUpdateToString:nCode];
[self.progressLabelArray insertObject:status atIndex:0];
[self.progressTableView reloadData];
[self.progressTableView setNeedsDisplay];
}
I guess, since the callbacks are coming on the same (UI thread), performSelectorOnMainThread may not be required.
My problem is that the table view only gets refreshed when the entire operation is complete, showing the last call back values. Is there a way by which I can force the UI to refresh after every callback? The callbacks coming might be relatively fast.
Resolved the issue by running the progress code in a separate thread. The callbacks come on a seprate thread and there I update my main thread (UI thread). I guess the UI thread was not getting any cycles to update the UI.
Put your
tableView.reloadData()
call in the following block and your tableview will refresh without any issue and without refreshing the scroller like in realtime apps.
For swift 3:
DispatchQueue.main.async()
{
tableView.reloadData()
}
For Objective C:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Background work
[tableView reloadData];
});
Related
I am using this Project from github, it is an image picker. I have had to make a very small change since ios7 to make the preview images from your albums show again but with that change now when you leave the picker and come back into it the photos selected (2/5) resets to 0/5 even though I have photos selected. How can I fix this?
The dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) seems to be taking forever to update the ui even with dispatch_async(dispatch_get_main_queue() to reload the ui inside of it. When I comment out the dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) the pictures load instantly but other things get broken that depend on the queue.
here is the code snippet with the dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) I changed with the code i changed commented out
AGIPCAssetsController.m:
- (void)loadAssets
{
[self.assets removeAllObjects];
__ag_weak AGIPCAssetsController *weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong AGIPCAssetsController *strongSelf = weakSelf;
#autoreleasepool {
[strongSelf.assetsGroup enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result == nil)
{
return;
}
AGIPCGridItem *gridItem = [[AGIPCGridItem alloc] initWithImagePickerController:strongSelf.imagePickerController asset:result andDelegate:strongSelf];
if ( strongSelf.imagePickerController.selection != nil &&
[strongSelf.imagePickerController.selection containsObject:result])
{
gridItem.selected = YES;
}
[strongSelf.assets addObject:gridItem];
}];
}
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf reloadData];
});
});
[strongSelf reloadData];
}
AGIPCGridItem is subclass of UIView. Don't work with UIKit objects on background thread.
Make sure you need the background thread and if you do, put only heavy tasks to background. Creating an UIView should not be that case.
Also, it's not recommended to use PRIORITY_LOW use simple PRIORITY_DEFAULT.
Edit: If you are curious why it did work on iOS 6: That's implementation detail of UIKit. It still was wrong, but somehow did what you expected.
I spent quite a bit of time with this code and I couldn't find a proper solution. Apparently the issue has come up on github, and a user offered a fix:
https://github.com/arturgrigor/AGImagePickerController/issues/19
But apparently he just removed all the blocks running in background, so I suppose that for a large amount of images the performance would be bad.
My hunch is that inside a dispatch_async block runs code that calls some UIKit function,
and thus the behaviour is basically undefined.
For example it seems to me that the setAsset function in AGIPGridItem.m is called inside the dispatch_async you posted. It is calling UImage, and although it's inside a lock, it should be still be executed on the background thread, while all the UIKit code should be executed on the main one.
UITableViewCell load images and reused cells
But even if I wrap the call inside a dispatch_async(dispatch_get_main_queue()...) it doesn't work yet.
It seems that the call [view removeFromSuperview]; in setItems in AGIPGridell.m is responsible somehow, but removing it has the side effect of creating a memory leak (unsurprisingly).
The global dispatch queue is a shared resource. DISPATCH_QUEUE_PRIORITY_LOW tasks run after every other task in the queue at a higher priority has run. If the queue is getting a lot of blocks submitted with a higher priority, your DISPATCH_QUEUE_PRIORITY_LOW task may not run for a very long time!
This is documented in the Concurreny Programming Guide as well as the libdispatch man pages
So, basically, other higher priority tasks are keeping things busy and your low priority task is not getting an opportunity to go.
I highlighted iMartin's answer "AGIPCGridItem is subclass of UIView. Don't work with UIKit objects on background thread." He's got it.
I had a very similar issue when moving iOS6 to 7. I was dispatching an ALAssets request in a background thread. Once the fetch completed, I would construct the UIImageView, a UILabel, and a wrapper and then send this object to the main/foreground thread to be rendered. This worked fine on iOS6, but on 7 it was not draw for about 20 seconds. It would sometime draw after a UI event like a touch.
Fix was to fetch the ALAsset in the background, send that to the main thread where I created the image view, etc. Works like a charm now.
How can I get an indication to main UI thread in an iPhone app IOS program, when background task has completed?
Background
I'm trying to setup a loading indicator per the concept at How to add a UIActivityIndicator to a splash screen in a iphone application?
was going to in AppDelete use "performSelectorInBackground" to load up the model data
what I need therefore is in the RootViewController some way of telling when the data has finished loading in the background so that it can (a) update the tableview with the data and (b) remove any activity indicator
I'm assuming way to do things here is as followings:
in App Delegate didFinishLaunchingWithOptions pass off Model data loading to background
AppDelegate loads up RootViewController and it immediately setup up an activity indicator
once data is loaded in background, it somehow has to indicate this back to the RootViewController (? reason for this question) that it's finished
2nd question is probably also when background task does inidicate its finished, how can the RootviewController check that the UI did get setup (with activity indicator etc) before it tries to disable the activity indicator
You can call back into the main thread from your background selector using -performSelectorOnMainThread:withObject:waithUntilDone: like so:
- (void)loadModel
{
// Load the model in the background
Model *aModel = /* load from some source */;
[self setModel:aModel];
[self performSelectorOnMainThread:#selector(finishedLoadingModel) withObject:nil waitUntilDone:YES];
}
- (void)finishedLoadingModel
{
// Notify your view controller that the model has been loaded
[[self controller] modelLoaded:[self model]];
}
Update: an even safer way to do this would be to check in -finishedLoadingModel to make sure you're running on the main thread:
- (void)finishedLoadingModel
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:YES];
}
// Notify your view controller that the model has been loaded
[[self controller] modelLoaded:[self model]];
}
Once you're finished loading in the background, call the following from your background thread:
[self performSelectorOnMainThread:#selector(backgroundLoadingDidFinish:) withObject:nil waitUntilDone:NO];
And then implement -(void)backgroundLoadingDidFinish:(id)sender in your RootViewController. If you need to, you can pass data back in the above method (the withObject: part).
Is this code using UIActivityIndicatorView flawed? It appears that I don't actually get to see the indicator/spinner at all here, so is this because the view isn't drawn until the who viewDidLoad completes?
Is the only way around this to do the viewDidLoad custom work (e.g. data updates) on a separate thread? (I was hoping in this case for an easier single-thread operation). Is there a way to force the view to refresh after the "startAnimating" line perhaps prior to the data loading commencment?
Code from UITableViewController implementation:
- (void)viewDidLoad {
// Wait indicator - Start
self.waitView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge] autorelease];
self.waitView.hidesWhenStopped = true;
[self.view addSubview: self.waitView];
// Load data into tableview
[NSThread sleepForTimeInterval: 5.0]; // Test code to simulate
[self.waitView stopAnimating];
}
You should also call startAnimating. Sleeping is not a good idea. I would prefer the performSelector-methods which starts a not recurring NSTimer under the hood.
Try this:
-(void) doStuff:(id)aSender
{
[self.waitView stopAnimating];
}
-(void)viewDidLoad
{
...
[self performSelector:#selector(doStuff:) withObject:self afterDelay:5.0];
}
in addtion: also set the frame- or bounds-property of the ActivityIndicatorView somewhere like sosborn said in his comment
Actually the answer from Thomas should work as it is, I will add a little explanation as to why not use sleep as you have done it.
All the UI processing on iPhone (and most of OSs as well) is being done in only one thread - the main thread, the thread that executes the so called run loop. If you stop that thread the UI will stop, nothing will be drawn.
Putting sleep into viewDidLoad, which runs in the main thread, will do just that - stop UI from doing anything. So because immediately after wakeup you've called [self.waitView stopAnimating] and the activityview should hide when not animating, you can't see it at all - you just didn't give it any time to show.
Thomas used a NSTimer to call stopAnimating after 5 seconds - now this lets the main thread to execute code before stopping animation and hiding waitView and this will work for your test.
Better yet you just let it animate without any timer and use a delegate patter to be informed by the tableView loading code after the data has been loaded, then stop animating. You don't know how long loading of data will last, so it's better to wait until it's finished than stop animating after any specific time.
Oh well, and the size and position, makes sense, but for testing it doesn't matter and is not the cause of not seeing it - if not specified it will be added at 0,0 and have a default size so you will see it anyway.
I have a UITableview that I load with data async so the tableview might appear without data.
I have tired the ReloadData method but the tableview remains empty until I scroll the tableview, suddenly the data appears.
The same thing happens when I load a tableview as a detailedview and switching between items, the previoud items data appears first and as soon as I scroll in the table view it shows the correct data.
My guess is that the ReloadData method works just fine, but I need to redraw the tableview somehow, any suggestions on how to solve this?
/Jimmy
You said you're populating content asynchronously but did you invoke the reloadData in the context of the main thread ? (and not via the thread that populates the content)
Objective-C
[yourUITableView performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:NO];
Swift
dispatch_async(dispatch_get_main_queue(), { self.tableView.reloadData() })
Monotouch
InvokeOnMainThread(() => this.TableView.ReloadData());
Yonels answer is perfect when your view is currently visible to the user (e.g: User presses a reload button which populates your UITableView.)
However, if your data is loaded asynchronously and your UITableView is not visible during the update (e.g: You add Data to your UITableView in another View and the UITableView is displayed later by userinput), simply override the UITableViewController's viewWillAppear method.
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
The positive effect is that your UITableView only reloads it's data once when the user actually want's to see it, not when new items are added.
I had the similar issue today(Storyboard in iOS 7.0).
I was using table View inside a UIViewController.When the view was loaded everything was working fine; all the delegates were getting called.However ; When the underlying dataSource(in my case,Array) was modified; the visible table rows were not getting updated.They were updated; only when I was scrolling the table view.
Tried everything; calling reloadData on Main thread;calling reload in ViewWillAppear; nothing worked.
The issue that I found ; was that I had not made the connection in storyboard; for the table view with the reference Outlet.The dataSource and delegate were set though.
I did not think that it could be the issue; as everything was working fine at the first go.
Hope it helps someone.I had some terrible time to find this out.
I guess it wasn't reloaded.
When you scroll the cells to out of screen, then…
tableView: cellForRowAtIndexPath:
…will be called. So it will be reloaded.
I guess UITableView variable is not validated.
If you use UITableView as a main view, you can try this.
[self.view reloadData];
or
[self.tableView reloadData];
Swift 4:
DispatchQueue.main.async { self.tableView.reloadData() }
After somewhat naively copying in yonel's solution and calling it good I realized that calling performSelectorOnMainThread:withObject:waitUntilDone: fixed the symptom, but not the problem. The bigger problem is that you are making UI updates while still in the context of the asynchronous or background thread.
This is what my code looked like:
dispatch_queue_t queue = dispatch_queue_create("com.kyleclegg.myqueue", NULL);
dispatch_async(queue, ^{
// Make API call
// Retrieve data, parse JSON, update local properties
// Make a call to reload table data
});
When it should look like this:
dispatch_queue_t queue = dispatch_queue_create("com.kyleclegg.myqueue", NULL);
dispatch_async(queue, ^{
// Make API call
// Retrieve data, parse JSON, update local properties
dispatch_async(dispatch_get_main_queue(), ^{
// Now make the call to reload data and make any other UI updates
[self.tableView reloadData]
});
});
If the only thing you need to do is call [self.tableView reloadData] it's probably fine to use performSelectorOnMainThread:withObject:waitUntilDone: since it accomplishes the same goal, but you should also recognize what's happening in the big picture. Also if you are doing more UI work than just reloading the table then all of that code should go on the main queue as well.
Reference: A concise example of using GCD and managing the background vs. main thread.
I have a UITableView with the following code:
- (void)viewDidLoad {
[super viewDidLoad];
parser = [[XMLParser alloc] init];
[parser parseXML];
My problem is that the launch takes too long because it's parsing everything before displaying the view controller with the UITableView. Also, if I set up another UITableView and parse another XML (in a different tab) I tap to go the other tab, but then it hangs while it parses the other XML, and once it's done, then it display the UITableView.
I have looked for information on when to start the parsing, reload the UITableView and how to show a loading screen while the parsing code runs, but have not been able to come up with anything.
Anyone have any ideas?
You can call something like
[parser performSelectorInBackground:#selector(parseXML) withObject:nil];
on your main thread to run the parseXML code in a different thread. Just be careful to not update the ui from that thread. To update the UI from the parser thread, you'll need to call something like
[self performSelectorOnMainThread:#selector(XMLUpdated:) withObject:self waitUntilDone:NO];
If by loading screen you mean an activity indicator then trying to add the indicator animated before parsing could potentially not work because when you parse on the main thread it blocks and does not let the indicator appear on screen. To get around this i would do the parsing on a background thread, this should allow your indicator to appear, when the parsing is done, ahve the parsing object send a message to your viewController so i t knows its ready to show the tableview. (i should mention that UIKit is not thread safe and you should not try to update any UI elements from the background thread without using performSelectorInMainThread)