I'm pretty new on iOS development and I'm having the following problem. I need to fill a UITableView with data fetched from a server.
I'm doing this call using AFNetworking. I want to show a "loading view" using SVProgressHUD and I'm doing the following:
-(void) viewDidLoad{
[super viewDidLoad];
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
[[MyServer sharedManager] fetchFromServerwithCompletionHandler:^(NSArray *elements, BOOL error) {
_elements = elements;
[_elementsTableView reloadData];
[SVProgressHUD dismiss];
}];
}
I'm getting the correct answer from the server but the progress hud is not being displayed. The call to the server does take some seconds so there is time for the progress hud view to be loaded but nothing happens.
I'm performing the call in the viewDidLoad because I need the call to be made as soon as the view is loaded and I want to call this just once.
If I move this block of code to the viewDidAppear the progress hud loads, but then this method is going to be called every-time the view is shown and I don't want this...
Thanks a lot!
The problem is not that the network call doesn't take place if done in viewDidLoad (because that is the right place to do it), but rather that the attempt to add the SVProgressHUD in viewDidLoad won't work, because this is too early in the process of constructing the view for SVProgressHUD to figure out where to put the HUD.
One simple solution would be to have viewDidLoad defer the invocation of this, to give the UI a chance to get to a state that SVProgressHUD can correctly place itself in the view hierarchy. As silly as it seems, you can just dispatch the code back to the main loop, which gives the UI a chance to catch up:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_main_queue(), ^{
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
[[MyServer sharedManager] fetchFromServerwithCompletionHandler:^(NSArray *elements, BOOL error) {
_elements = elements;
[_elementsTableView reloadData];
[SVProgressHUD dismiss];
}];
});
}
A trick can be use NSUserDefaults to save a value like #"serverAlreadyCalled" and check on viewDidAppear if you called the server before.
example:
-(viewDidAppear){
NSUserDefaults* defaults = [NSUserDefaults standardDefaults];
if ([defaults valueForKey:#"serverAlreadyCalled"] == nil) {
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
[[MyServer sharedManager] fetchFromServerwithCompletionHandler:^(NSArray *elements, BOOL error) {
_elements = elements;
[defaults setValue:#"YES" forKey:#"serverAlreadyCalled"];
[_elementsTableView reloadData];
[SVProgressHUD dismiss];
}];
}
}
Don't forget to set nil to your key #"serverAlreadyCalled" before your app finishes run.
Maybe it's not the best solution, but can make the trick.
Hope it helps.
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 :) )
Here is my situation:
I have a viewController whose main goal is to upload one photo. I use ReactiveCocoa like this:
- (IBAction)uploadTapped
{
[[PhotosService uploadSignal] subscribeError:^(NSError *error) {
NSLog(#"%#", error);
} completed:^{
NSLog(#"Upload completed");
}];
//I don't need to wait the upload result
[self.navigationController popViewControllerAnimated:YES];`
}
Is it ok to pop the viewController even if the upload isn't finished? I wouldn't want to risk an EXC_BAD_ACCESS, or a memory leak because of that...
Thank you for your help
I'm showing a UIActivityIndicator while saving a PFObject (using Parse.com)
- (void) saveNewMessage {
// Show Activity Indicator
UIActivityIndicatorView *activityView=[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityView.center=self.view.center;
[activityView startAnimating];
// Create Message
...
// Save Message
[message saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// complete activity indicator
[activityView stop];
}
}];
}
My Problem: What if the user leaves the view while we're still saving, and then comes back to the view and the save hasn't completed yet. The UIActivityIndicatorView will be gone. How do I determine whether the parse operation has completed?
It depends on how you implemented "leaving the view" and "coming back to the view"
If you use the same viewcontroller (for example pushing popping the vc with navigation viewcontroller), UIActivityIndicatorView has a method named
- (BOOL)isAnimating
You can check UIActivityIndicatorView's status
Otherwise (ie you allocate and present a new viewcontroller) you need to use a global mechanism, nsnotificationcenter might suit your needs or you may set a variable in AppDelegate or you can read/write to a local file.
You could store a BOOL in your view controller telling that the save operation has finished, then update it in your completion block along with the [activityView stop];.
Then, to handle the case where the user leaves the screen and comes back later, add a check in - (void)viewWillAppear to also hide the UIActivityIndicatorView if the save operation has completed (e.g. if your BOOL is set to YES).
PFObject actually has a built in boolean to tell us when its done saving. You may also make your save button disabled at the same time just to keep from getting multiple save requests.
[activityView startAnimating]
[yourobject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error){
if (succeeded) {
[activityView stopAnimating];
}else{
[activityView stopAnimating];
}
I have a strange problem with my tableView.
I load data via JSON into my tableView. While the JSON is being requested from the web in another class, I show an activity indicator view in my current view and the tableView is hidden.
I ve got a delegate method, which is called as soon as the json is ready.
-(void)didReceivePlayers:(NSArray *)players {
[activityIndicator stopAnimating];
tableViewPlayers.hidden = false;
startButton.hidden = false;
playersData = [[NSMutableArray alloc] initWithArray:players];
[tableViewPlayers reloadData];
NSLog(#"done reloading");
}
The method is being called perfectly.
The code is pretty straight forward. I hide my activity indicator and show my tableView.
Then I call reloadData. It takes only a few milliseconds. BUT after reloadData, my activityIndicator is still shown and it takes several seconds to show my tableview, although the nslog is being called right away.
I also tried calling reload data in mainThread, but this did not change a thing.
Thanks for your help!
Be sure that the code is being executed on the main thread. You can use the main operation queue like this:
-(void)didReceivePlayers:(NSArray *)players {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[activityIndicator stopAnimating];
tableViewPlayers.hidden = false;
startButton.hidden = false;
playersData = [[NSMutableArray alloc] initWithArray:players];
[tableViewPlayers reloadData];
NSLog(#"done reloading");
}];
}
I have a modal view controller that appears, checks a service on the Internet and then dismisses itself when done. The nib contains an activity indicator and a label to inform the user what is going on.
When the update is complete, the label changes to "Update Complete" and then dismisses the view controller. However, I want it to delay the dismiss for a couple of seconds to give the user a chance to see the text before it disappears. So I've done this:
#pragma mark - AssetLoaderServiceDelegate
- (void)assetLoaderServiceDidFinishLoading:(AssetLoaderService *)service
{
[self.spinner stopAnimating];
self.infoLabel.text = #"Update complete";
[self performSelector:#selector(dismissUpdater) withObject:nil afterDelay:2.0];
}
- (void)dismissUpdater
{
[self dismissModalViewControllerAnimated:YES];
}
But for some reason, the selector is never being called. I've tried running it in mode NSRunLoopCommonModes too, but that doesn't work either.
I must be doing something wrong, but I can't see what...
EDIT: The delegate callback is actually happening within an NSOperationQueue, which might mean it's not on the same thread when it sends the message back to the view controller? So I tried
[self performSelector:#selector(downloadQueueComplete) withObject:nil afterDelay:0.0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
followed by
- (void)downloadQueueComplete
{
[delegate assetLoaderServiceDidFinishLoading:self];
}
But the performSelector doesn't seem to be working here either.
Following up your suggestion about the thread issue, would you try with:
[self performSelectorOnMainThread:#selector(downloadQueueComplete) withObject:nil waitUntilDone:YES]];
Sorted! In the AssetLoaderService, I had to perform selector on main thread:
[self performSelectorOnMainThread:#selector(downloadQueueComplete) withObject:nil waitUntilDone:YES];
After that, all the following calls were on the correct thread. :)