View not updating, but everything on the main thread - iphone

I'm trying to show a UIProgressView on top of a table view after certain user interactions. In my table view, if the user taps one particular cell, I slide up a UIView that contains a toolbar with bar items and a picker view (it very much behaves like an action sheet). When I do this, I'm adding the view offscreen to the current view, and then animating the slide-in. When the user makes a selection and taps "done", the view slides back out. At that point, the progress view is supposed to appear and update as some things are happening in the background.
The problem I'm having is that after the "alert sheet" UIView is slided out of the current view, nothing happens in the UI for a good while. When I show the sliding view, I do this:
[[[UIApplication sharedApploication] keyWindow] addSubview:slidingView];
CGRect newFrame = CGRectMake(...);
[UIView animateWithDuration:0.3
animations:^{
slidingView.frame = newFrame;
}];
When the user taps the 'done' button in the sliding view, this action method is invoked:
- (void) done {
NSNumber *row = GetSelectedRowSomehow();
[self dismiss:#selector(doneCallback:) withObject:row];
}
- (void) dismiss:(SEL)cb withObject:(id)obj {
[UIView animateWithDuration:0.3
animations:^{
slidingView.frame = CGRectMake(...);
}
completion:^(BOOL finished) {
[self.delegate performSelectorOnMainThread:cb
withObject:obj
waitUntilDone:NO];
[slidingView performSelectorOnMainThread:#selector(removeFromSuperview:)
withObject:nil
waitUntilDone:NO];
}];
}
The callback that gets called here:
- (void) doneCallback {
self.dialogView.hidden = NO;
self.progressView.progress = 0;
for (float i = 0; i < 1.0; i += 0.1) {
self.progressView.progress += i;
sleep(0.5);
}
}
In my case, dialogView doesn't appear until after callback has completed. Why wouldn't it just update the display immediately after its hidden property was set to NO?

The display is updated in the main thread once your code finishes executing, so by blocking the main thread you're preventing that happening.
One reason for that is that the updates are batched together so they can be redrawn efficiently.
One solution/workaround would be to use performSelector:afterDelay:0.01 to in doneCallback after the hidden=NO, moving the following code into a new selector that runs after the delay.
Alternatively you could run the code within "doneCallback" as a background operation instead (though you cannot directly update UIKit from the background thread, so you'd have to send messages to the main thread for any display updates).

Related

UINavigationController undoes changes to previous UIView

I have a navigation controller with a couple of view controllers in it. In the first view controller (the app's initial one) I perform an animation, which moves two UIImageViews around, and fades in some additional ui elements. Now, when I push the next view controller and then go back to the initial one, the transformations are gone and the image views are exactly where they were when loaded from the storyboard. The additional ui elements, however, are still visible and so is the text I've entered in some UITextFields. So the view controller is not entirely reset to its initial state but somehow the performed animations are undone. Can anybody tell me whether this is regular behavior and - if so - how I can preserve the transformations?
A little more disclosure: I'm simply setting off a regular animation upon hitting a button...
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
self.firstView.center = firstCenter;
self.secondView.center = secondCenter;
}
completion:^(BOOL finished) {
// Upon complection fade in additional ui elements
[UIView animateWithDuration:0.4
animations:^{
self.firstElement.alpha = 1.0;
self.secondElement.alpha = 1.0;
self.thirdElement.alpha = 1.0;
}
completion:^(BOOL finished) {
[self.activityIndicator stopAnimating];
}];
}];
... and then pushing the next view controller after hitting another button.
[self.navigationController pushViewController:_nextViewController animated:YES];
As far as I can see I'm not doing anything funky here, so any help is appreciated.

iPhone iOS what is the correct way to do actions with UIView animateWithDuration block?

I'm wandering what is the correct way to do "long" actions in response to user events. For example, I have this slide to cancel block that animates itself off screen over 0.5 seconds. The [self coreDataAction] may take about 0.3 seconds itself.
I want to ensure that the action completes once the user sees the end of the animation ( I do not want the user to accidentally navigate to a different controller or close the app thinking that the action is done).
Where should I put the [self coreDataAction]; in this case? Above the block, within the block or in the completion block?
//should I put it here?
CGPoint slideToCancelCenter = slideToCancel.view.center;
[UIView animateWithDuration:0.5 animations:^{
self.goToSleepButton.center = slideToCancelCenter;
[UIView setAnimationDuration:0.5];
CGPoint sliderCenter = slideToCancel.view.center;
sliderCenter.y += slideToCancel.view.bounds.size.height;
slideToCancel.view.center = sliderCenter;
//should I put it here?
// [self coreDataAction];
} completion:^(BOOL finished) {
//should I put it here?
} ];
A better way to handle this might be to animate the view on-screen then start the coreDataAction in the completion handler. Once the coreDataAction method execution is complete you can call a method to animate the slide to cancel view off-screen.
Assuming [self coreDataAction] executes on the main thread, I would say you should put it on the first line to ensure that the method is complete by the time the animation is done.

Pass Control (Responder?) back to Lower UIView whilst Still Performing Fade Out Animation on Upper UIView

From all of the view controllers within my application if I am processing a long running task I present the user a 'progress view'. This is a UIView that lives in my MainWindow.xib. I show (fade in) the view using an AppDelegate method...
- (void)showProgressView
{
self.progressView.hidden = NO;
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 1.0; }];
}
When the long running task has finished I fade out the 'progress view' with the following AppDelegate method...
- (void)hideProgressView
{
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
My problem is that as the progress view fades away and as the buttons/controls in the under-lying view below become visible again they ARE NOT usable (they don't respond to touch events) until the animation has fully finished and the 'progress view' is hidden.
Is there anyway for me to pass control back to the underlying view, before starting the fade out animation, so that its buttons etc do work whilst the progress view fades away?
EDIT: Things I have already tried unsuccessfully...
Using resignFirstResponder
- (BOOL)findAndResignFirstResponder:(UIView*)view
{
NSLog(#"looping %#", [view description]);
if (view.isFirstResponder)
{
[view resignFirstResponder];
return YES;
}
for (UIView *subView in view.subviews)
{
if ([self findAndResignFirstResponder: subView])
{
return YES;
}
}
return NO;
}
- (void)hideProgressView
{
// Recursively attempt to remove control from the progress view
BOOL result = [self findAndResignFirstResponder:self.progressView];
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
Using endEditing
- (void)hideProgressView
{
// Attempt to remove control from the progress view
[self.progressView endEditing:YES];
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
You can try sending your progress view the message resignFirstResponder. This should make the view below it the first responder, so you can use its controls.
PS: I also think that maybe your progress view could be filling the whole display; in this case, changing the first responder might not help...
EDIT: after you confirmed that your view is taking the full screen...
If your view is full screen, it is intercepting all the touches that you do (because when it is not fully hidden/transparent it is covering the views behind it). You have two options, either you make the view smaller, so that you have no overlapping, or you make so that the touches are forwarded to the view behind it.
You can do the latter in several ways, I hope that one works for you:
you can try and override in your progress view (it needs be a custom UIView), the touchesBegan method;
you can try and override the hitTest method in your progress view;
This is what I would try, e.g. in touchesBegan:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self viewIsDisappearing])
[self.nextResponder touchesBegan:touches withEvent:event];
}
viewIsDisappearing is a method you should implement to return YES if the animation to hide the progress view has already begun. During the animation the view is not yet hidden, so it will intercept touches, and you forward those touches to the next responder.
It is possible that you also need to override the other UIResponder's touche-related methods:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
– touchesCancelled:withEvent:
EDIT:
I have found a class of mine where I do something similar to what I am suggesting here, only, without using the nextResponder.
The idea is: SDSTransparentView is a UIView that covers the whole screen. You initialize it like this:
[[SDSTransparentView alloc] initWithContent:contentView andDelegate:delegate];
The delegate implements an SDSTransparentViewProtocol which simply contains one method:
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event outsideOfView:(UIView*)view;
When the user touches anywhere on the transparent view, it relays the touch to the delegate by calling the protocol method. I would suggest you to ignore the contentView and outsideOfView arguments (they where useful for me, but possibly not for you; you can either pass nil or, better, the view behind the progress view).
You can find the class on my github. You only need the SDSTransparentView.* files. Actually, I only suggest having a look at how the class is implemented (very short) and do the same in your progress view.
I can assure that this approach works!

UIView block animation transitions with animated content on showing/hiding keyboard

In my app I have a text field on some view which is covered by the keyboard when it shows up. So I have to scroll the view (or even rearrange the subviews). To do this I:
register for keyboard notifications:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moveViewUp)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moveViewDown)
name:UIKeyboardWillHideNotification
object:nil];
upon receiving a notification, move the view using block animations like this:
- (void)moveViewUp {
void (^animations)(void) = nil;
oldViewFrame = self.view.frame;
animations = ^{
CGRect newViewFrame = oldViewFrame;
newViewFrame.origin.y -= kViewOffset;
self.view.frame = newViewFrame;
};
[UIView animateWithDuration:1.0
animations:animations];
}
- (void)moveViewDown {
void (^animations)(void) = nil;
animations = ^{
self.view.frame = oldViewFrame;
};
[UIView animateWithDuration:1.0
animations:animations];
}
This works fine, the view scrolls up and down, until I add some more animation. Specifically I'm adding a transition to a next view when the user taps a button:
- (IBAction)switchToNextView:(id)sender {
// [self presentModalViewController:nextViewController animated:YES];
[UIView transitionFromView:self.view
toView:self.nextView
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromRight
completion:nil];
}
Now we got to the problem.
If the first view was shifted when the button was tapped (that means that the keyboard was visible), the transition to the next view starts simultaneously as the keyboard slides down, but the view itself doesn't move down, so for a split second we can actually see the underlying view. That's not right. When I present the next view modally (see the commented line) all animations go as I want them to: i.e. the keyboard is hiding, the view is flipping from right and scrolling down -- all at the same time. This would be fine, but the problem is that I actually don't have a UIViewController for that view. In fact I'm trying to simulate the modal behavior without UIViewController (why so? perhaps it's just a bad design, I'll post another question on that).
So why does in this case the animation from moveViewDown method is not triggered at the proper time?
Update 1
I added a debug print to each function to check the order of calling, this is what I get:
-[KeyboardAnimationViewController moveViewUp]
__-[KeyboardAnimationViewController moveViewUp]_block_invoke_1 <-- scroll up animation
-[KeyboardAnimationViewController switchToNextView:]
-[KeyboardAnimationViewController moveViewDown]
__-[KeyboardAnimationViewController moveViewDown]_block_invoke_1 <-- scroll down animation
Even if I explicitly move the view down before the transition like this
- (IBAction)switchToNextView:(id)sender {
// [self presentModalViewController:nextViewController animated:YES];
NSLog(#"%s", __PRETTY_FUNCTION__);
if (self.view.frame.origin.x < 0)
[self moveViewDown];
[UIView transitionFromView:self.view
toView:self.nextView
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromRight
completion:nil];
}
I get exactly the same log.
Update 2
I've experimented some more and made following conclusions:
If I call moveViewDown or resignFirstResponder: explicitly, the animation is postponed until the end of current run loop, when all pending animations actually start to play. Though the animation block logs to the console immediately -- seems strange to me!
The method transitionFromView:toView:duration:options:completion: (perhaps transitionWithView:duration:options:animations:completion: too, didn't check this one) apparently makes a snapshot of the "from-view" and the "to-view" and creates an animation using these snapshots solely. Since the scrolling of the view is postponed, the snapshot is made when the view is still offset. The method somehow disregards even the UIViewAnimationOptionAllowAnimatedContent option.
I managed to get the desired effect using any of animateWithDuration: ... completion: methods. These methods seems to disregard transition options like UIViewAnimationOptionTransitionFlipFromRight.
The keyboard starts hiding (implicitly) and sends corresponding notification when removeFromSuperview is called.
Please correct me if I'm wrong somewhere.
If you are trying to simulate the modal behavior without UIViewController I guess you want your next view to show up from the bottom of the screen right?. correct me if I am wrong.
If you want such an animation you can try a work-around where you change the frame of the next view within an animation block such that it appears as if its similar to presentModalViewController

iPhone Busy View

I am trying to get a busy view displayed during a search process however it never seems to display.
What I am trying to achieve
User clicks on search button once they have entered the text in a UISearchBar.
The text entered is delegated to searchBarSearchButtonClicked.
Show a "Busy view" which is a UIView containing a UIActivityIndicatorView to indicate the search is underway.
Perform a search which communicates with a website.
On search completion remove the "Busy View".
Whats wrong
The search process is working fine, using the debugger I can see it moving through the searchBarSearchButtonClicked function fine however the "Busy View" never appears. The search takes 2-3 seconds so in theory I should see my "Busy View" appear for those 2-3 seconds.
Code Attempt 1 - Add and remove the "Busy View" from the superview**
- (void)searchBarSearchButtonClicked:(UISearchBar *)activeSearchBar {
[activeSearchBar setShowsCancelButton:NO animated:YES];
[activeSearchBar resignFirstResponder];
[busyView activateSpinner]; //start the spinner spinning
[self.view addSubview:busyView]; //show the BusyView
Search *search = [[Search alloc]init];
results = [search doSearch:activeSearchBar.text];
[busyView deactivateSpinner]; //Stop the spinner spinning
[busyView removeFromSuperview]; //remove the BusyView
[search release]; search = nil;
}
Results Of Attempt 1 - It does not appear, however if I comment out the removeFromSuperview call the Busy View remains on screen.
Code Attempt 2 - using animations
- (void)searchBarSearchButtonClicked:(UISearchBar *)activeSearchBar {
[activeSearchBar setShowsCancelButton:NO animated:YES];
[activeSearchBar resignFirstResponder];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop)];
[busyView activateSpinner]; //start spinning the spinner
[self.view addSubview:busyView]; //show the busy view
[UIView commitAnimations];
Search *search = [[Search alloc]init];
results = [search doSearch:activeSearchBar.text];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop)];
[busyView deactivateSpinner]; //sets stops the spinner spinning
[busyView removeFromSuperview]; //remove the view
[UIView commitAnimations];
[search release]; search = nil;
}
Results of attempt 2 - Did not see the "Busy View" appear
Well, Here's my solution:
Create singleton class MyLoadingView. This class should contain show and hide static methods.
In MyLoadingView constructor you should manually create a view with semi-transparent black background and activityview inside of it. Place this view into [[UIApplication sharedApplication] keyWindow]. Hide it.
[MyLoadingView show] invocation just brings myLoadingView object to front in it's container: [[[UIApplication sharedApplication] keyWindow] bringSubviewToFront:myLoadingViewSharedInstance];. Also don't forget to start activityview animation.
[MyLoadingView hide] stops activityview animation and hides sharedInstanceView.
Please notice, that you need to invoke [MyLoadingView show] in a separate thread.
UI changes in Cocoa only take effect the next time your code returns control to the run loop. As long as your search task runs synchronously on the main thread, it will block execution, including UI updates.
You should execute the time-consuming task asynchronously in the background. There are a number of options for this (NSOperation, performSelectorInBackground:..., Grand Central Dispatch, ...).