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".
Related
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.
Basically I have a main screen that has a button on it, when you click on that button I want to load a list of users from a server and display them on the next screen in the tableview.
I can get the data with no issues, and pass it to my tableview with no issues - my problem lies with loading the data into the cells after I have received the data!
Processes exist like this:
Tap button
Starts NSURLConnection
Opens up UITableView on screen
Loads nothing
Data returns, adds to NSArray
Tableview Reload
In viewWillAppear - make local users NSArray equal received Data
Nothing loads.
If I then press back, then press the button again, all my cells are populated with the data I received before.
Thanks in advance for any help. I've been searching around for a while now :(
Edit:
When moving from main screen to the tableview
Note: getUsers sets up the NSURLConnection and starts the connection
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([segue.identifier isEqualToString:#"selectUser"]) {
DDNetworkRequest *networkRequest = [[DDNetworkRequest alloc] init];
[networkRequest getUsers];
}
}
Tableview class:
Note: returnUserList just returns the array of data which is set as a variable within the network class
-(void)viewWillAppear:(BOOL)animated{
DDNetworkRequest *networkRequest = [[DDNetworkRequest alloc] init];
users = [networkRequest returnUserList];
}
Network Class:
note: returnUsers manipulates and then saves the NSArray variable
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSDictionary *jsonDictionary;
jsonDictionary = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
[usersJSONDict isEqualToDictionary: jsonDictionary];
[self returnUsers:jsonDictionary];
DDUserMessage *userMessageTable = [[DDUserMessage alloc] init];
[userMessageTable.tableView reloadData];
}
** Edit2 **
returnUser method
-(void) returnUsers:(NSDictionary*)userDict{
userListArray = [userDict valueForKey:#"username"];
}
Without any code provided from your side, I guess u forgot this awesome operation inside the HTTP get request success block:
[self.tableView loadData];
M I RIGHT? ;)
After you receive the data, you have to pass it to the data source array, then call
[self.tableView reloadData];
Also make sure you call this line from the main thread. You cannot make changes to UI from another thread.
I have two ViewController and use a (tableview click) seque for opening the second ViewController.
My Problem is, the Second View Controller load much Data. So the time between switch is <> 10 Seconds. In this 10 Seconds the App freeze. Thats OK, but HOW can i insert a "Popup" or "Alert" Message like "Please Wait..." BEVOR . I have testing much tutorials for Popups and Alerts, but the Popup/Alter shows only, when the SecondView Controller is complete loaded. I will show the Message BEVOR the SecondViewController is compled loaded.
Example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// IF i set here the ALERT, the Alter was only show, when the Second View Controller is complete loaded!
NSDictionary *rowVals = (NSDictionary *) [SearchNSMutableArray objectAtIndex:indexPath.row];
[self performSegueWithIdentifier:#"Foo" sender:self];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"Foo"]) {
// Get indexpath from Tableview;
NSIndexPath *indexPath = [self.SearchUITableView indexPathForSelectedRow];
// Get Data from Array;
NSDictionary *rowVals = (NSDictionary *) [self.SearchNSMutableArray objectAtIndex:indexPath.row];
// Destination View;
[MySecondViewController alloc];
MySecondViewController *MyView = (MySecondViewController *)segue.destinationViewController;
}
}
You are trying to fix the problem with the wrong solution. That solution is just as bad because the popup will also freeze for 10 seconds. What if you add more data and it takes 30 seconds or 10 minutes? Are you going to expect your users to see a dialog they can't dismiss for 10 minutes?
Are you fetching the data from the internet? If so you need to fetch your data asynchronously in the background.
If you're loading it from disk then there's too much being loaded that could possibly be displayed on one screen, you need to load only a small portion of it, and if that still takes a long time you need to load it asynchronously.
UPDATED -
You should have a model class for your application that is responsible for fetching the data from the internet.
Google Model View Controller to get some background information on what a Model is.
As soon as the app launches the model can start to download the data which needs to be down in the background (that's too big a topic to answer how to do that here).
The View controller can launch while the data is being downloaded and it can display a spinning activity indicator wheel or progress bar or dialog etc. while waiting. The important thing is the GUI will not freeze.
When the model has downloaded the data it needs to tell the view controller the data is now available, which it can do using NSNotification center.
There's lots for you to investigate and learn, to do it without GUI freezing it needs to be done properly, there's no shortcut, you have a lot to study.
#Martin,
i found a solution:
// Send the Request;
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
So the request are asynchrony. Thanks for your answer. Great +1
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 am navigating by clicking a button to a viewcontroller where I am loading webview,but after clicking the button it is taking some time,how to navigate faster and load webview faster,please help.I have only the following code in second viewcontroller.
-(void)viewWillAppear:(BOOL)animated{
self.navigationController.navigationBarHidden=YES;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://myurl"]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.wb loadRequest:request] ;
});
});
}
Try this code
-(void)viewWillAppear:(BOOL)animated{
self.navigationController.navigationBarHidden=YES;
dispatch_queue_t jsonParsingQueue = dispatch_queue_create("jsonParsingQueue", NULL);
// execute a task on that queue asynchronously
dispatch_async(jsonParsingQueue, ^{
NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://myurl"]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.wb loadRequest:request] ;
});
});
}
If I understand your question, there isn't much you can do to make if faster. That request speed is based on internet speed (Over which you don't have much of a control).
Also the request already happens asynchronously, so there's no need to do that yourself.
You are combining two things as you navigate to your webview
loading and displaying a view
Retrieving data from the internet
You can only directly influence the first one, the second one is well beyond your control.
By performing the asynchronous NSURLRequest from within the viewWillAppear method, you are telling iOS to delay showing the view until the internet has given you all the data it needs.
A better approach is to configure all the visual elements of your new view, display that view in the interface, and then AFTER the new view is visible, perform your NSURLRequest.
Adding a UIActivityIndicator may also help your users realize that your app was snappy and responsive, and the delay they are experiencing is from the internet.
Perhaps the easiest way to fix this would be to move your code over to
- (void)viewDidLoad {}