I have one problem.
I have table view and when I click on cell I load data from server. Because this could take some time I want to show activity indicator view.
-(void)startSpiner{
CGRect screenRect = [[UIScreen mainScreen] bounds];
UIView * background = [[UIView alloc]initWithFrame:screenRect];
background.backgroundColor = [UIColor blackColor];
background.alpha = 0.7;
background.tag = 1000;
UIActivityIndicatorView * spiner = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spiner.frame = CGRectMake(0, 0, 50, 50);
[spiner setCenter:background.center];
[spiner startAnimating];
[background addSubview:spiner];
[background setNeedsDisplay];
[self.view addSubview:background];
}
This works fine, but when I put this in
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self startSpiner];
Server *server = [[Server alloc]init];
self.allItems = [server getDataGLN:self.object.gln type:1];
}
I see that UIActivityIndicatorView is shown after it get data from server.
How to force main view to update immediately?
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self startSpiner];
////If your server object is performing some network handling task. dispatch a
////background task.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
Server *server = [[Server alloc]init];
self.allItems = [server getDataGLN:self.object.gln type:1];
});
}
The UI normally isn't updated until your code returns control to the run loop. Your getDataGLN:type: method isn't returning until it gets the data from the server. Thus the UI cannot be updated until you've got the data from the server.
Don't do that. Load your data on a background thread and return control of the main thread to the run loop immediately. You will find lots of help in the Concurrency Programming Guide and in Apple's developer videos.
Why do you call setNeedsDisplay?
Anyway, The right way to do it, is not creating all the UI when you want to show loading indicator.
Do it in advance - in ViewDidLoad for example, and just hide the background view.
When you want to show it, just turn it's hidden property to NO.
Related
So I am trying to create somewhat of a loading screen in my application. I have a view that takes 3-10 seconds to load. During this time I want to display a UIView that I made that is simply black with a loading screen. Currently I am putting my code inside of the viewDidLoad function just after super viewDidLoad. Here is my code
UIView* baseView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:baseView];
[baseView setBackgroundColor:[UIColor blackColor]];
[self.view bringSubviewToFront:baseView];
baseView.layer.zPosition = 1;
This works and creates my view overtop of everything exactly how I want it, however this waits until my main view is completely done loading before it actually shows anything. Is viewDidLoad not a good place to put this and if so where should I put it.
Simply put I have a very basic UIView that I want to load while I wait for the actual view to load and then simply hide it. Any ideas how to do this?
You need to dispatch your hard work on another thread, otherwise the OS will wait until all the process is done before refreshing the UI (that's why you see your loading screen appear only after 3-10 seconds).
Don't forget to dispatch back on the main thread after the lengthy job is done. All UI updates must be done on the main thread.
self.loadingView.hidden = NO;
// Dispatch lengthy stuff on another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Do lengthy stuff here
// Dispatch back on the main thread (mandatory for UI updates)
dispatch_async(dispatch_get_main_queue(), ^{
self.loadingView.hidden = YES;
});
});
UIView* baseView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:baseView];
[baseView setBackgroundColor:[UIColor blackColor]];
[self.view bringSubviewToFront:baseView];
baseView.layer.zPosition = 1;
//Create new dispatch for load data
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Load data in here
// Call main thread to update UI
dispatch_async(dispatch_get_main_queue(), ^{
baseView.hidden = YES;
});
});
I have reviewed many different ways to get the activity indicator to start however I feel synchronising it correctly seems a bit challenging. Nothing is broken with the below code however the Activity monitor icon(throbber) doesn't show up until the last moment before transitioning to the next view.
When the user taps the one of the elements within the UITable it kicks off getting a JSON response then takes the user to the next View. Works perfectly. Except the ActivityIndicator is late to show up as stated before.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = spinner;
[spinner startAnimating];
timer = [NSTimer scheduledTimerWithTimeInterval:3.0/1.0 target:self selector:#selector(loading) userInfo:nil repeats:YES];
//Go to the next view
if ([cell.textLabel.text isEqualToString:#"Vatsim Pilots"]) {
VatsimViewController *detail = [self.storyboard instantiateViewControllerWithIdentifier:#"newview"];
[self.navigationController pushViewController:detail animated:YES];
}
}
I have attempted to use the NSStimer function but clearly that doesn't work. I have seen others do this but not hitting a JSON service. I would have thought it wouldn't matter where the data is coming from and only cares when the data is ready.
Thanks in advance!
The problem is that the spinner doesn't start spinning until after the current CATransaction ends - after all your code has finished. But your code doesn't finish until the JSON response comes back, because (in code apparently not shown here) you do this interaction synchronously.
Don't. You should not be networking synchronously! You are blocking the main thread when you do that - and if you do that for too long, the WatchDog process will kill your app dead.
Thus the fact that the spinner doesn't start is actually symptomatic of something much deeper that you're doing wrong.
Having said that, the solution to your problem is to start the spinner spinning and then immediately get off the main thread, so that the CATransaction has a chance to end. The spinner will then actually start spinning! Then when your code re-enters you can start your time-consuming navigation, as in this code (from my book):
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// ... start spinner here ...
// now get off main thread
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// ... now do time-consuming stuff ...
// ... and finally, navigate:
UIViewController *detailViewController = [ViewController new];
[self.navigationController pushViewController:detailViewController animated:YES];
});
}
When loading my UIViewController, I basically put a spinner in the middle of the page until the content loads, then come back on the main thread to add the subviews :
- (void)viewDidLoad {
[super viewDidLoad];
UIActivityIndicatorView *aiv_loading = ... // etc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Load content
NSString *s_checkout = [[BRNetwork sharedNetwork] getCheckoutInstructionsForLocation:self.locBooking.location];
UIView *v_invoice_content = [[BRNetwork sharedNetwork] invoiceViewForBooking:locBooking.objectId];
// Display the content
dispatch_sync(dispatch_get_main_queue(), ^{
if (s_checkout && v_invoice_content) {
[aiv_loading removeFromSuperview];
[self showContentWithText:s_checkout AndInvoice:v_invoice_content];
} else {
NSLog(#"No data received!"); // is thankfully not called
}
});
});
}
- (void) showContentWithText:(NSString *)s_checkout AndInvoice:(UIView *)v_invoice {
[self.view addSubview:[self checkoutTextWithText:s_checkout]]; // Most of the time displayed text
[self.view addSubview:[self completeCheckout]]; // always Displayed UIButton
[self.view addSubview:[self divider]]; // always displayed UIImageView
// Summary title
UILabel *l_summary = [[UILabel alloc] initWithFrame:CGRectMake(0, [self divider].frame.origin.y + 6 + 10, self.view.bounds.size.width, 20)];
l_summary.text = NSLocalizedString(#"Summary", nil);
[self.view addSubview:l_summary];
CGRect totalRect = CGRectMake([self divider].frame.origin.x, [self divider].frame.origin.y + 6 + 30, self.view.bounds.size.width - [self divider].frame.origin.x, 90);
_v_invoice = v_invoice;
_v_invoice.frame = totalRect;
[self.view addSubview:[self v_invoiceWithData:v_invoice]]; // THIS Pretty much never displayed
UITextView *l_invoice = [[UITextView alloc] initWithFrame:CGRectMake(0, _v_invoice.frame.origin.y + _v_invoice.frame.size.height + offset, 320.0, 50)];
l_invoice.text = NSLocalizedString(#"summary_emailed", nil);
[self.view addSubview:l_invoice]; // Always displayed
}
However, not all the content is displayed. The invoice is never there at first, but gets displayed after a couple of minutes. The other async-created string, s_content is sometimes not displayed.
This seems to be random with the content creation. The end result is pretty neat, but not reliable for a production version.
I used the undocumented [self.view recursiveDescription] to check if everything was there, and even if I don't see it, they are all there with what seems to be correct frames.
Any pointers?
- layoutSubviews did not help!
- putting a background color to the invoice view is showing the background color
I suspect this line is your problem:
UIView *v_invoice_content = [[BRNetwork sharedNetwork] invoiceViewForBooking:locBooking.objectId];
As you are calling this in a background dispatch queue. Any work involving UIKit should be done on the main queue/thread. Either move that into the main thread block, or if building the view is dependent on data from a network call, change your invoiceViewForBooking method to return the data first, and build your view in the main thread with that data.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Load content
NSString *s_checkout = [[BRNetwork sharedNetwork] getCheckoutInstructionsForLocation:self.locBooking.location];
id someData = [[BRNetwork sharedNetwork] invoiceDataForBooking:locBooking.objectId];
// Display the content
dispatch_async(dispatch_get_main_queue(), ^{
UIView *v_invoice_content = [invoiceViewWithData:someData];
});
});
I'd also suggest using dispatch_async instead of dispatch_sync on the main queue.
Solved! I ended up removing the dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
To only leave the async block on the main queue:
// Display the content from a new async block on the main queue.
dispatch_async(dispatch_get_main_queue(), ^{
[aiv_loading removeFromSuperview];
NSString *s_checkout = [[BRNetwork sharedNetwork] getCheckoutInstructionsForLocation:self.locBooking.location];
[self showContentWithText:s_checkout AndInvoice:[[BRNetwork sharedNetwork] invoiceViewForBooking:locBooking.objectId]];
});
which did the trick, viewcontroller's view can appear without waiting for the view to load the remote data!
You should put your code in viewDidAppear instead of viewDidLoad. Whatever is related to display (even launching async blocks) should always be in viewWillAppear or viewDidAppear.
Also, I recommend you to use dispatch_async(dispatch_get_main_queue(), ^{}) instead of your dispatch_sync since you still want your block to be performed async (but on main thread).
Dont forget to call super methods in your viewDidAppear.
I copied the code to show the activity indicator from this post. When I called hideActivityViewer nothing happens, and the activityView is still there, and the activityIndicator is still spinning. It is as if hideActivityViewer does not do anything to the activityView at all.
Here is the code I have modified
-(void)showActivityViewer
{
WBAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = delegate.window;
_activityView = [[UIView alloc] initWithFrame: CGRectMake(0, 0, window.bounds.size.width, window.bounds.size.height)];
_activityView.backgroundColor = [UIColor blackColor];
_activityView.alpha = 0.5;
UIActivityIndicatorView *activityWheel = [[UIActivityIndicatorView alloc] initWithFrame: CGRectMake(window.bounds.size.width / 2 - 12, window.bounds.size.height / 2 - 12, 24, 24)];
activityWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
activityWheel.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin);
[_activityView addSubview:activityWheel];
[window addSubview: _activityView];
[[[_activityView subviews] objectAtIndex:0] startAnimating];
}
-(void)hideActivityViewer
{
NSLog(#"Profile: hiding Activity Viewer");
[[[_activityView subviews] objectAtIndex:0] stopAnimating];
[_activityView removeFromSuperview];
_activityView = nil;
}
Update:
I'm using the KVO to detect for change in variable and use it to call showActivityViewer.
Turns out, showActivityViewer was called more than once, as a result there are multiple activityViewer on the screen, so when I remove one, the other is still there and I have no to reference to it. I solved this by checking if the activityView already exist, and if so don't create a new one.
My comment as an answer:
It could be because activityView becomes nil at some point before a call to hideActivityViewer. Lets say u call showActivityViewer two times consecutively, you have two activityViews exactly on top of each other and the first one will never be hidden if you call hideActivityViewer. Even after matching number of hideActivityViewer calls, or more.
You may be calling ur method "hideActivityViewer" from asynchronous or thread completion blocks.
If so, call ur method on mainthread i.e
[self performSelectorOnMainThread:#selector(hideActivityViewer) withObject:nil waitUntilDone:NO];
I've put EGOTableViewPullRefresh in my project. However, I've noticed one troubling thing. If I ever need to download a (semi-moderately) large file by pulling down to refresh, EGOTableViewPullRefresh says that I am done downloading much faster than I really am.
Is there a way to make EGOTableViewPullRefresh show that is is still downloading. In other words, I would like the words "loading" to last longer on the screen when I pull down to refresh. Actually, I would like "loading" to be on the screen until it is done loading.
In viewDidloadmethod:
if (_refreshHeaderView == nil) {
EGORefreshTableHeaderView *view = [[EGORefreshTableHeaderView alloc] initWithFrame:CGRectMake(0.0f, 0.0f - self.tableView.bounds.size.height, self.view.frame.size.width, self.tableView.bounds.size.height)];
view.delegate = self;
[self.tableView addSubview:view];
_refreshHeaderView = view;
}
// update the last update date
[_refreshHeaderView refreshLastUpdatedDate];
You should set pullTableIsRefreshing to NO in block
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//your data fetching code
dispatch_async(dispatch_get_main_queue(), ^{
//your UI code
self.tableView.pullTableIsRefreshing = NO;
});
});