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;
});
});
Related
I implemented pull to refresh functionality for my controller:
- viewDidLoad {
self.refresh = [[UIRefreshControl alloc] init];
[self.refresh addTarget:self action:#selector(refreshData) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:self.refresh];
}
- (void) refreshData {
__weak myController *weakSelf = self;
[[NetworkcallManager instance] getCall:^(NSString *result){
if(weakSelf != nil) {
[weakSelf.refresh endRefreshing];
[weakSelf.tableView reloadData];
}
}];
}
The refresh works as expected, but when I pull down the screen too hard then the entire screen disappears for a moment and then the data gets refreshed.
Is there a way to avoid disappearance of the screen? I am wondering if it is because of reloadData function. In case it calls 'viewWillAppear' then I can understand the problem I am facing. (My viewWillAppear code results in momentarily blank screen).
Edit:
If I pull down without internet connection (turned off the wifi) then the screen doesn't disappear.
I got the issue. In the code I am purging the table and then updating it. As a result, the screen disappears momentarily.
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 am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.
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 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.