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];
}
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 :) )
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.
I have been reading up on Objectice-C blocks as I have been running into them more and more lately. I have been able to solve most of my asynchronous block execution problems, however I have found one that I cannot seem to fix. I thought about making an __block BOOL for what to return, but I know that the return statement at the end of the method will be executed before the block is finished running. I also know that I cannot return a value inside the block.
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"Reminder Segue"]) {
eventStore = [[EKEventStore alloc] init];
[eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) {
if (!granted) {
UIAlertView *remindersNotEnabledAlert;
remindersNotEnabledAlert = [[UIAlertView alloc] initWithTitle:#"Reminders Not Enabled" message:#"In order for the watering reminder feature to function, please allow reminders for the app under the Privacy menu in the Settings app." delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
//I would like to put a simple return NO statement here, but I know it is impossible
}
}];
}
return YES;
}
How do I create a simple return statement from a block?
While the immediate idea might be to make your asynchronous request synchronous, that's rarely a good idea, and do to so in the middle of a segue, such as this case, is likely to be problematic. It's almost never a good idea to try to make an asynchronous method synchronous.
And, as smyrgl points out, the idea of "can't I just return a value from the block" is intuitively attractive, but while you can define your own blocks that return values (as Duncan points out), you cannot change the behavior of requestAccessToEntityType such that it returns a value in that manner. It's inherent in its asynchronous pattern that you have to act upon the grant state within the block, not after the block.
So, instead, I would suggest a refactoring of this code. I would suggest that you remove the segue (which is likely being initiated from a control in the "from" scene) and not try to rely upon shouldPerformSegueWithIdentifier to determine whether the segue can be performed as a result of a call to this asynchronous method.
Instead, I would completely remove that existing segue and replace it with an IBAction method that programmatically initiates a segue based upon the result of requestAccessToEntityType. Thus:
Remove the segue from the button (or whatever) to the next scene and remove this shouldPerformSegueWithIdentifier method;
Create a new segue between the view controllers themselves (not from any control in the "from" scene, but rather between the view controllers themselves) and give this segue a storyboard ID (for example, see the screen snapshots here or here);
Connect the control to an IBAction method, in which you perform this requestAccessToEntityType, and if granted, you will then perform this segue, otherwise present the appropriate warning.
Thus, it might look something like:
- (IBAction)didTouchUpInsideButton:(id)sender
{
eventStore = [[EKEventStore alloc] init];
[eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) {
// by the way, this completion block is not run on the main queue, so
// given that you want to do UI interaction, make sure to dispatch it
// to the main queue
dispatch_async(dispatch_get_main_queue(), ^{
if (granted) {
[self performSegueWithIdentifier:kSegueToNextScreenIdentifier sender:self];
} else {
UIAlertView *remindersNotEnabledAlert;
remindersNotEnabledAlert = [[UIAlertView alloc] initWithTitle:#"Reminders Not Enabled" message:#"In order for the watering reminder feature to function, please allow reminders for the app under the Privacy menu in the Settings app." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[remindersNotEnabledAlert show];
}
});
}];
}
You CAN return a value from a block, just like from any function or method. However, returning a value from a completion block on an async method does not make sense. That's because the block doesn't get called until after the method finishes running at some later date, and by then, there is no place to return a result. The completion method gets called asynchronously.
In order to make a block return a value you need to define the block as a type that does return a value, just like you have to define a method that returns a value.
Blocks are a bit odd in that the return value is assumed to be void if it's not specified.
An example of a block that returns a value is the block used in the NSArray method indexOfObjectPassingTest. The signature of that block looks like this:
(BOOL (^)(id obj, NSUInteger idx, BOOL *stop))predicate
The block returns a BOOL. It takes an object, an integer, and a pointer to a BOOL as parameters. When you write a block of code using this method, your code gets called repeatedly for each object in the array, and when you find the object that matches whatever test you are doing, you return TRUE.
If you really want to make a block synchronous (although I question the validity of doing so) your best bet is to use a dispatch_semaphore. You can do it like this:
dispatch_semaphore_t mySemaphore = dispatch_semaphore_create(0);
__block BOOL success;
[eventStore requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError *error) {
success = granted;
dispatch_semaphore_signal(mySemaphore);
}];
dispatch_semaphore_wait(mySemaphore, DISPATCH_TIME_FOREVER);
However again I don't think you want to do this, especially in a segue as it will stall the UI. Your better bet is to rearchitect what you are doing so that you don't have a dependency on the async process being completed in order to continue.
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'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.