I have a view controller with labels, textfields, activity indicator and a button control all tied with IBOutlets and accessible from within my code.
When the user presses the button, I hide a few of the fields, and put up the activity indicator. I then make a synchronous URL call to get some JSON data.
The view controller is not updated to reflect the activity indicator and hidden fields until AFTER the synchronous request returns. I need this to happen before the request returns so the users sees that something is happening.
I have tried putting in a usleep(200000);, and also tried [self.view setNeedsDisplay]; - - both to no avail.
Is there any way I can force the screen update BEFORE the blocking synchronous call? I know I can go to an async call, but I really don't want to do that since I cannot do anything in the app until/unless I get the data i need...
Thanks,
Jerry
here is the code I use to send the sync request: This is the relevant portion of the routine 'SendGetEventsRequest'.
NSURLResponse *response = nil;
NSError *error = nil;
[self.aiActivityIndicator startAnimating];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
Here are the other routines i use right before the above routine gets called:
- (IBAction)btnGetEvents:(id)sender {
// The user presssed the GetEvents button. Send the requested server to be processed.
[self.aiActivityIndicator startAnimating];
[self.lblCollectingEventNames setHidden:NO];
[self.lblEnterServerName setHidden:YES];
[self.btnGetEvents setHidden:YES];
[self.tfServerName setHidden:YES];
[self.view setNeedsDisplay];
[self SendGetEventsRequest:self.tfServerName.text];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
[self.tfServerName resignFirstResponder];
[self btnGetEvents:self];
return YES;
}
Basically, when the user presses the 'go' button on the keyboard, the routine textFieldShouldReturn gets called. I dismiss the keyboard then simulate pushing the button 'Get Events'.
In btnGetEvents, I start up the activity indicator, hide/show a few fields, then call sendGetEventsRequest. In there is the code where i do the Sync call.
I am setting up the activity indicator and show/hide fields BEFORE the sync call, yet they are not updated until AFTER the sync call returns. I believe this is because the view controller did not perform a screen redraw before the Sync call got executed.
So, I need to figure out how to get the screen to update BEFORE the Sync call. I hope the additional code and additional explanation helps.
Wow, I cannot believe I am the only person to have this issue. I modified my code to use the async call and it is working perfectly.
Related
I'm making a synchronize function that syncs local Core Data with the server. I want to make the synchronizations happen in the background without disrupting user interaction. When I receive the response (whether success or failure) the app should display a message somewhere on the screen to notify the user about the outcome.
UIAlertController is not a good choice because it will block user action.
Currently I'm using SVProgressHUD:
__weak StampCollectiblesMainViewController *weakSelf = self;
if ([[AppDelegate sharedAppDelegate] hasInternetConnectionWarnIfNoConnection:YES]) {
[_activityIndicator startAnimating];
[Stamp API_getStampsOnCompletion:^(BOOL success, NSError *error) {
if (error) {
[_activityIndicator stopAnimating];
[SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeClear];
[SVProgressHUD setAnimationDuration:0.5];
[SVProgressHUD showErrorWithStatus:#"error syncronize with server"];
}
else {
[_activityIndicator stopAnimating];
[featuredImageView setImageWithURL:[NSURL URLWithString:[Stamp featuredStamp].coverImage] usingActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[yearDropDownList setValues:[Stamp yearsDropDownValues]];
[yearDropDownList selectRow:0 animated:NO];
[weakSelf yearDropDownListSelected];
[SVProgressHUD dismiss];
}
}];
}
Is there a modification I can make so the user can still interact with the app? I just want to show the message without taking up too much space. Any help is much appreciated. Thanks.
Looks like the easiest thing will be to use SVProgressHUDMaskTypeNone.
Also check out this issue.
Sorry but you gonna have to build your own custom view.
In fact it's not that difficult. What I would do is simply add a small view on the top of the screen with your custom message and a close button (to allow user to hide quickly the message). This is usually done by adding this new view to the current window, so that it will be on the top of every view and won't block the UI (except the part hidden by that view :) )
in my application i'm using a UIButton (myButton) wich i added via Storyboard.
So, when tapping the button an IBAction gets called:
-(IBAction)buttonAction:(UIButton*)sender{
[myCustomClass doSomeCrazyStuff];
[sender setEnabled:NO];
}
As you can see, i'm calling a method from myCustomClass ( myCustomClass is a REST-Client for my web-service).
The viewController the button lays in is delegate of myCustomClass.
There are two delegate methods implemented, one for success and one for error.
-(void)requestSucceeded{
/* If the request succeeded i want the button to be enabled again, and it's selected
state inverted */
NSLog(#"This gets called");
[myButton setEnabled:YES];
[myButton setSelected:!myButton.selected];
}
This works totally fine: i press the button, stuff is done on myCustomClass, request succeeds, button is set to inverted selected state.
But now for the other delegate method:
-(void)requestFailed{
/* If the request failed i want the button to be enabled again, and it's selected
state stays the same */
NSLog(#"That gets called");
[myButton setEnabled:YES];
}
If requestFailed gets called, the console prints That gets called as expected, but the button stays disabled... and i don't know why.
I tried other things in requestFailed like:
[myButton setHidden:YES];
Just to see if the reference to myButton is working...
And it is.
Probably i'm missing something right now, but i can't figure it out.
Thanks for your help.
EDIT:
I don't think requestFailed could be called from a different thread (as #gonji-dev mentioned), since both requestSucceeded and requestFailed are called from the same method.
In my doSomeCrazyStuff method i set up a completion block wich handles connection success and error. If an error occurred it gets handled in another class. If the connection succeeded i'm asking for HTTP status codes to decide wether requestFailed or requestSucceeded will be called.
Similar situation as in https://stackoverflow.com/a/31952060/218152.
Are you absolutely positive that you are invoking:
[myButton setEnabled:YES];
on the main thread?
The industry standard, when manipulating the UI in response to notifications or multithreaded environment is:
dispatch_async(dispatch_get_main_queue(), ^{
// update UI
});
In our iPgone & iPad app we use push segue transitions between different ui contollers, most of them extend UICollectionViewController. In each controller we load data from our internal API. Loading is done viewWillAppear or viewDidLoad.
Now, the thing is, that this API call sometime can take a second or two, or even three... well, lot's of stuff there, let's assume we can't change it. But, we can change the user experience and at least add the "loading" circle indicator. The thing is, what I can't understand by means of correct concept, while transition from A to B, the "load" is done at B, while page A still presented.
So, question is "how do I show indicator on page A, while loading controller for page B?"
Thanks all,
Uri.
Common approach in this case is to load data in destination view controller NOT in main thread. You can show indicator while loading data in background thread and then remove it.
Here is sample of code from my project solving the same problem:
- (void) viewDidLoad {
...
// add indicator
self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.spinner.hidesWhenStopped = YES;
self.spinner.center = self.view.center;
[self.view addSubview:self.spinner];
...
// fetch news
[self.spinner startAnimating];
__weak typeof(self) weakSelf = self
[[BitrixApiClient sharedInstance] getLatestNewsWithCompletionBlock:^(NSArray *newsArray, NSUInteger maxPageCount, NSUInteger currentPageNumber, NSError *error) {
if (!error) {
weakSelf.newsArray = newsArray;
weakSelf.currentPageNumber = currentPageNumber;
[weakSelf.newsTableView reloadData];
}
// stop spinning
[weakSelf.spinner stopAnimating];
}];
}
I loaded a UIView from a UIViewController. This UIView contains a (big) UICollectionView.
The transition from the first UIView to the second UIView is very slow: It seems that when the rendering of all collection's cells is done the second view can show up.
In the second UIView, I tried.
- (void)viewDidAppear:(BOOL)animated
{
[activityView stopAnimating];
NSLog(#"did appear %#",[NSDate date]);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[activityView startAnimating];
NSLog(#"will appear %#",[NSDate date]);
}
In the NSLog, there is no time difference between the two events, and in fact the second UIView shows up in about 1 second after the event viewDidAppear.
At this point, I would start a UIActivityIndicator, as in the code. But the indicator is never shown.
Any hint?
Your problem here is that you're probably blocking the main thread by maybe doing some disk IO or network activity or heavy computations, and that is why you're experiencing this delay.
I'd recommend that you do all this on a secondary thread while showing a UIActivityIndicator. On the completion you can then hide the activity indicator and show the collection view.
EDIT:
N.B. There is probably a better way to go, but i'm not very familiar with collection views.
A really easy fix would be to keep a BOOL ivar in the view controller where you load the collection view. Call it shouldLoadData and set it to NO in your viewDidLoad method. Then all you need to do is to return 0 to your UICollectionViewDelegate methods numberOfSectionsInCollectionView: and collectionView:numberOfItemsInSection:.
Finally in your viewDidAppear method, you set shouldLoadData to YES and call reloadData on your collectionView. The tricky part at this point is to figure out a way to tell when the collection view finished reloading its data so that you can stop the activity indicator.
I found out that it is not even that tricky, reloadData just queues up on the main thread, so you can just queue another task on the main thread after you make the call to reloadData. Just do:
[self.collectionView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.activity stopAnimating];
});
And you'll get the desired behaviour. You should be aware, however, that this would still block the main thread.
E.g. if you have a back button, it could not be pressed until the data is fully loaded (it could actually be pressed, but it would not have any visible effect until then).
We're having a problem displaying a view above a tableView on iOS. Our approach
is to create a UIView that is a subview of a sublass of UIViewController, send
it to the back, and then bring it to the front upon didSelectRowAtIndexPath.
We're using an XIB to create the user interface. The view hierarchy is like
this:
View
-- UIView ("loading..." view)
-- -- UILabel ("loading...")
-- -- UIActivityIndicatorView
-- UITableView
-- UILabel
Here is what we're doing to try to display the "loading" view:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Create a request to the server based on the user's selection in the table view
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
NSError *err;
// Show the "loading..." message in front of all the other views.
[self.view bringViewToFront:self.loadingView];
[self.loadingWheel startAnimating];
// Make the request
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&err];
// Stop animating the activity indicator.
[loadingWheel stopAnimating];
// other stuff...
}
Whenever we leave the "loading" view at the front of all the other views in the
XIB, we can see that it looks as we want. However, when we leave the loading
view at the back (per the view hierarchy above) and then try to bring it to the
front, the view never displays. Printing out self.view.subviews shows that our
loading view is in fact in the view hierarchy. Interestingly, if we try to
change something else in our view within didSelectRowAtIndexPath (for example,
changing the background color of a label that's already displaying in the view),
the change never shows on the simulator.
The problem is the synchronous request. It blocks the main thread, and so the activity indicator does not get a chance to show.
A simple solution would be to asynchronously load the data on a global queue, and when everything is loaded, call back to the main queue.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Make the request
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&err];
dispatch_async(dispatch_get_main_queue(), ^{
// Stop animating the activity indicator.
[loadingWheel stopAnimating];
// other stuff...
});
});
While the solution above works, it blocks a global queue and so it is not ideal. Have a look at the asynchronous loading via NSURLConnection. It is explained in great detail in Apple's "URL Loading System Programming Guide".