I create a UIView contentView and display it. I then fetch data from server and create a bunch of subviews displaying the data. I am using MBProgressHUD to display while waiting on data.
if (datasetSubBar.panels == nil) {
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:datasetSubBar.filterListView animated:YES];
HUD.labelText = #"Creating Panels";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[datasetSubBar createPanels];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:datasetSubBar.filterListView animated:YES];
});
});
}
in createPanels method, i fetch the data then create the panels. The panel is created, added to filterListView (the content view) and then add the constraints:
for (int i = 0; i < panels.count; i++) {
NSLog(#"thread: %#", [NSThread currentThread]);
NSDate *startDate = [NSDate date];
DatasetFilterListPanelView *panel = [panels objectAtIndex:i];
[contentView addSubview:panel];
// add constraints to position each panel
}
these are run on a separate thread which is what I believe the issue is. The UI can only be updated on the main thread.
I tried adding:
dispatch_async(dispatch_get_main_queue(), ^{
[contentView addSubview:panel];
});
But that raises errors for the constraints (the constraints don't have reference to it since its in a different thread).
If I run createPanels on the main thread, the panels will display but it also locks up the UI until its complete.
Any ideas?
I'm not sure which constraints are violated, but you could also try:
[contentView performSelctorOnMainThread:#selector(addSubview) withObject:panel waitUntilDone:YES];
instead of
dispatch_async(dispatch_get_main_queue(), ^{
[contentView addSubview:panel];
});
Not sure whether this will help.
Related
I have table view that load new data (depend on page) from SQL data base. Problem is, when i load it in main thread, it block UI for a while. When i try to do "hard work" in background, and reload data in main thread, odd things start to happen, for example, table view section header move in wrong place, and i load enormous amount of data.
First case, all work but block UI for while:
[self.tableView addInfiniteScrollingWithActionHandler:^{
#strongify(self)
if (!self.viewModel.isUpdating){
self.viewModel.isUpdating = YES;
[self.tableView.infiniteScrollingView startAnimating];
[self.viewModel nextPage];
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
}
}];
In second case, i tried to do background work, following not work as expected:
if (!self.viewModel.isUpdating){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Add some method process in global queue - normal for data processing
self.viewModel.isUpdating = YES;
[self.tableView.infiniteScrollingView startAnimating];
[self.viewModel nextPage];
dispatch_async(dispatch_get_main_queue(), ^(){
//Add method, task you want perform on mainQueue
//Control UIView, IBOutlet all here
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
});
//Add some method process in global queue - normal for data processing
});
}
}];
How should i modify my code to not load main thread, and without "weird" things?
have you tried something like this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
.....
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelectorOnMainThread:#selector(updateView) withObject:nil waitUntilDone:YES];
});
});
......
-(void)updateView{
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
}
user PerformSelectorOnMainThread it may help you.
I have added the loading bar in below snippet.It’s worked fine which I have deleted small amount of data(say 30 to 150 mb). But the problem is when I deleted large size of data, the loading bar doesn't get loaded(approx 190mb).
dispatch_async(dispatch_get_main_queue(), ^{
[self addloading];
if(_isProgress)
return;
_lastDeleteItemIndexAsked = index;
NSInteger catalogue_id =[[[_currentData objectAtIndex:index%[_currentData count]] valueForKey:#"catalogue_id"] integerValue];
BOOL update_avilable = [[[online_date_array objectAtIndex:index%[_currentData count]] valueForKey:#"update_version"] boolValue];
if(update_avilable)
[[self GridViewDelegate] deletecatlogue_for_update:catalogue_id];
else
[[self GridViewDelegate] deletecatlogue:catalogue_id];
[online_date_array replaceObjectAtIndex:index%[online_date_array count] withObject:[NotificationHandler check_online_date_of_catalogue:catalogue_id]];
[_gmGridView reloadObjectAtIndex:index%[_currentData count] withAnimation:GMGridViewItemAnimationFade];
[self stopLoadingfor_delete];
});
Is it size or execution time?
I'm going to assume that self is a UI object. If it is, the block should be:
Controller* __weak weakSelf = self;
dispatch_async(queue, ^{
Controller* strongSelf = weakSelf;
if (strongSelf) {
...
}
else {
// self has been deallocated in the meantime.
}
});
Thus you may prefer a MVVC solution, where a view model does the hard work, not the view controller.
Note: please don't comment on whether or not this is a good idea ;) I'm just experimenting so want to see how it pans out...
I've got a UITextView and some inputText, which I'm going to set as the text for the UITextView.
I want to try visually drip-feeding the inputText to the text view, say, one letter at a time or one word at a time.
For example (without any consideration for the drip-feed delay or thread of execution):
for (NSInteger i = 0; i < inputText.length; i++) {
NSString* charAtIndex = [text substringWithRange:NSMakeRange(i, 1)];
_textView.text = [NSString stringWithFormat:#"%#%#", _textView.text, charAtIndex];
}
So, to build in a delay and see the characters (or words) added, one at a time, I can do the following:
dispatch_queue_t backgroundQueue = dispatch_queue_create("background_queue", 0);
for (NSInteger i = 0; i < inputText.length; i++) {
dispatch_async(backgroundQueue, ^{
[NSThread sleepForTimeInterval:0.01f];
NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
dispatch_async(dispatch_get_main_queue(), ^{
_textView.text = [NSString stringWithFormat:#"%#%#", _textView.text, charAtIndex];
});
});
}
That works fine, and as expected, the for loop is queueing up a bunch of asynchronous operations on the background queue.
But I want the above to take place as part of a larger, single synchronous visualisation. Before adding this code to drip-feed the UITextView, I simply set the text in the UITextView and then animated the current view (including the UITextView) off-screen. All on the main thread. The user would tap a button, see the whole text appear and then the view starts to animate off the screen, right away. The user then moves on to the next step in the workflow.
I'm trying the drip-feed visualisation to give the impression of the text view being populated one character (or word) at a time, but I don’t want the user to have to wait until every last character/word has been added. So, I’d like to see the UITextView being populated for say, 0.3 or 0.5 of a second, before starting the animation that animates the view (including the UITextView) off-screen…
So it’s kinda like this:
dispatch_queue_t backgroundQueue = dispatch_queue_create("background_queue", 0);
for (NSInteger i = 0; i < inputText.length; i++) {
dispatch_async(backgroundQueue, ^{
[NSThread sleepForTimeInterval:0.01f];
NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
dispatch_async(dispatch_get_main_queue(), ^{
_textView.text = [NSString stringWithFormat:#"%#%#", _textView.text, charAtIndex];
});
});
}
// ...
// Animate the view (including the UITextView) off screen
// ...
// User continues with the main workflow
Right now, with all that drip-feeding happening asynchronously in the background, once I add in the code to animate the view out of the way, you miss the visual drip-feed. The main thread runs right through to animating the view off screen.
I’m not sure how to achieve what I want?
Do I interrupt the above loop in some way? Check for a flag that’s updated from another thread?
I can’t put the wait on the main thread - because it will prevent the drip-feed updates to the UITextView…
Any suggestions?
You can delay the animation to "give" time to the drip-feed, like this:
double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// ...
// Animate the view (including the UITextView) off screen
// ...
});
This way the user will see the drip-feed animation for 0.5s. This is the GCD version but you can use the method too:
[UIView animateWithDuration:0.3f // Set your duration here
delay:0.5f
options:UIViewAnimationOptionCurveEaseOut // Choose the right option here
animations:^{
// Do your animations here.
}
completion:^(BOOL finished){
if (finished) {
// Do your method here after your animation.
}
}];
You're better off not queuing up loads of tasks and then setting up another delayed animation. Yes, it will work, but it isn't so maintainable and doesn't cater well for other situations in the future.
Instead, think about using an NSTimer and a couple of instance variables (which could be wrapped up in another class to keep everything clean and tidy). The instance variable is basically the current progress (i from your loop). Each time the timer fires, check i - if the text animation isn't complete, use i to substring and update the UI. If the text animation is complete, invalidate the timer and start the final view animation.
In this way the logic is organised, easily understandable, reusable and cancellable.
Based on what J. Costa suggested, I've stayed with the Grand Central Dispatch approach. The piece that I was missing (and I'm not sure that I explained this requirement very well) is the access to a shared resource.
In the following code, I've structured it so:
BOOL shared resource to indicate whether or not the drip-feed should continue
create a serial queue for reading from / writing to that shared resource
the drip-feed happens on a background queue and additionally uses the serial queue to check if it should continue
the delayed view animation is setup using dispatch_after, and when it occurs it uses the serial queue to signal the drip-feeding should stop
Code:
NSString* inputText = #"Some meaningful text...";
dispatch_queue_t serialQueue = dispatch_queue_create("serialqueue", DISPATCH_QUEUE_SERIAL);
// the shared resource
_continueDripFeed = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSInteger i = 0; i < inputText.length; i++) {
__block BOOL keepGoing = NO;
dispatch_sync(serialQueue, ^{
// read from the shared resource
keepGoing = _continueDripFeed;
});
if (keepGoing) {
[NSThread sleepForTimeInterval:0.02f];
NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
dispatch_async(dispatch_get_main_queue(), ^{
_textView.text = [NSString stringWithFormat:#"%#%#", _textView.text, charAtIndex];
});
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
_textView.text = inputText;
});
break;
}
}
});
double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
dispatch_sync(serialQueue, ^{
// update the shared resource
_continueDripFeed = NO;
});
[self animateTextViewToFrame:_offScreenFrame];
// Continue with workflow...
});
I am having some trouble updating my UI using performSelectorOnMainThread. Here is my situation. In my viewDidLoad I set up an activity indicator and a label. Then I call a selector to retrieve some data from a server. Then I call a selector to update the UI after a delay. Here's the code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.reloadSchools = [[UIAlertView alloc] init];
self.reloadSchools.message = #"There was an error loading the schools. Please try again.";
self.reloadSchools.title = #"We're Sorry";
self.schoolPickerLabel = [[UILabel alloc]init];
self.schoolPicker = [[UIPickerView alloc] init];
self.schoolPicker.delegate = self;
self.schoolPicker.dataSource = self;
self.server = [[Server alloc]init];
schoolList = NO;
_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:_activityIndicator];
[self.view bringSubviewToFront:_activityIndicator];
[_activityIndicator startAnimating];
[NSThread detachNewThreadSelector: #selector(getSchoolList) toTarget: self withObject: nil];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:20.0];
}
The selector updateUI checks to see if the data was retrieved, and calls a selector on the main thread to update the UI accordingly. Here is the code for these parts:
-(void)updateUI
{
self.schools = [_server returnData];
if(!(self.schools == nil)) {
[self performSelectorOnMainThread:#selector(fillPickerView) withObject:nil waitUntilDone:YES];
}
else {
[self performSelectorOnMainThread:#selector(showError) withObject:nil waitUntilDone:YES];
}
}
-(void)showError {
NSLog(#"show error");
[_activityIndicator stopAnimating];
[self.reloadSchools show];
}
-(void)fillPickerView {
NSLog(#"fill picker view");
schoolList = YES;
NSString *schoolString = [[NSString alloc] initWithData:self.schools encoding:NSUTF8StringEncoding];
self.schoolPickerLabel.text = #"Please select your school:";
self.shoolArray = [[schoolString componentsSeparatedByString:#"#"] mutableCopy];
[self.schoolPicker reloadAllComponents];
[_activityIndicator stopAnimating];
}
When the selector fillPickerView is called the activity indicator keeps spinning, the label text doesn't change, and the picker view doesn't reload its content. Can someone explain to me why the method I am using isn't working to update my ui on the main thread?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//load your data here.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI in main thread.
});
});
First of all you should not be using detachNewThreadSelector. You should use GCD and submit your background task to an async queue. Threads are costly to create. GCD does a much better job of managing system resources.
Ignoring that, your code doesn't make a lot of sense to me. You submit a method, getSchoolList, to run on a background thread. You don't show the code that you are running in the background.
Then use performSelector:withObject:afterDelay to run the method updateUI on the main thread after a fixed delay of 20 seconds.
updateUI checks for self.schools, which presumably was set up by your background thread, and may or may not be done. If self.schools IS nil, you call fillPickerView using performSelectorOnMainThread. That doesn't make sense because if self.schools is nil, there is no data to fill the picker.
If self.schools is not nil, you display an error, again using performSelectorOnMainThread.
It seems to me that the logic on your check of self.schools is backwards. If it is nil you should display an error and if it is NOT nil you should fill the picker.
Next problem: In both cases you're calling performSelectorOnMainThread:withObject:waitUntilDone: from the main thread. Calling that method from the main thread doesn't make sense.
Third problem: It doesn't make sense to wait an arbitrary amount of time for a background task to run to completion, and then either succeed or fail. You won't have any idea what's going on for the full 20 seconds. If the background task finishes sooner, you'll never know.
Instead, you should have your background task notify the main thread once the task is done. That would be a valid use of performSelectorOnMainThread:withObject:waitUntilDone:, while calling it from the main thread is not. (Again, though, you should refactor this code to use GCD, not using threads directly.
It seems pretty clear that you are in over your head. The code you posted needs to be rewritten completely.
I'm trying to update my textView on screen before it starts downloading data. Right now, it only updates the view after all of the downloads are complete. How can I do it before or in between the downloads?
Edit: I want the self.textView.text = #"Connection is good, start syncing..."; to update the UI before the downloading starts. But right now, it only updates after the download finishes.
Here is what the code looks like.
if ([self.webApp oAuthTokenIsValid:&error responseError:&responseError]) {
self.textView.text = #"Connection is good, start syncing...";
[self.textView setNeedsDisplay];
[self performSelectorInBackground:#selector(downloadCustomers:) withObject:error];
}
I'm new to this and have yet to learn how threads work, but from what I read, the downloadCustomers function should be using a background thread leaving the main thread to update the UI.
if ([self.webApp oAuthTokenIsValid:&error responseError:&responseError]) {
self.textView.text = #"Connection is good, start syncing...";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self downloadCustomers];
dispatch_async(dispatch_get_main_queue(), ^{
//Do whatever you want when your download is finished, maybe self.textView.text = #"syncing finished"
});
});
}
The pattern here is to initialize your download on background thread and then call back to main thread for UI update.
Below is an example using GCD. The advantage of GCD version is that you can consider using whatever you do in -downloadCustomers, to insert in-line where you call it.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self downloadCustomers];
dispatch_async(dispatch_get_main_queue(), ^{
[self.textView setNeedsDisplay];
});
});