Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

Long story short, I have a view controller where the user can tap on self.view (anywhere but the nav bar) and it will enter a full screen mode where the controls at the bottom fade out and the navigation and status bar fade out. Similar to iBooks.

I could simply fade the alpha of the navigation bar, but as to allow the user to tap in the newly gained area (where the navigation bar was now that it's faded out) and have it do something, I have to do more than change the alpha, as the nav bar is still technically there taking up area.

So I hide the navigation bar with [self.navigationController setNavigationBarHidden:YES animated:NO];. I have to do this after the animation block finishes, else it will be in the animation block and animate as part of the block. So I use a dispatch_after to make it finish after the animation completes (0.35 second delay).

However, this causes the issue where if the user taps any time during that 0.35 second time period where it's animating out and things are waiting to be finished, it causes glitchy behaviour where another block starts even though it's still waiting 0.35 seconds for the other one to finish. It causes some glitchy behaviour and causes the navigation bar to stay hidden. Gross.

Video of it happening: http://cl.ly/2i3H0k0Q1T0V

Here's my code to demonstrate what I'm doing:

- (void)hideControls:(BOOL)hidden {
    self.navigationController.view.backgroundColor = self.view.backgroundColor;
    int statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;

    [UIView animateWithDuration:0.35 animations:^{
        [[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];

        if (hidden) {
            self.navigationController.navigationBar.alpha = 0.0;
            self.instructionsLabel.alpha = 0.0;
            self.backFiftyWordsButton.alpha = 0.0;
            self.forwardFiftyWordsButton.alpha = 0.0;
            self.WPMLabel.alpha = 0.0;
            self.timeRemainingLabel.alpha = 0.0;
        }
        else {
            self.navigationController.navigationBar.alpha = 1.0;
            self.instructionsLabel.alpha = 1.0;
            self.backFiftyWordsButton.alpha = 1.0;
            self.forwardFiftyWordsButton.alpha = 1.0;
            self.WPMLabel.alpha = 1.0;
            self.timeRemainingLabel.alpha = 1.0;
        }

        [self.view layoutIfNeeded];
    }];

    // Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
    if (hidden) {
        double delayInSeconds = 0.35;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
            self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;
        });
    }
    else {
        [self.navigationController setNavigationBarHidden:NO animated:NO];
        self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;
    }
}

The only other thing I'm doing is changing the constant on my Auto Layout constraint to account for the navigation bar and status bar dependent on whether or not they're there.

I'm not sure how to factor in the fact that double tapping can really glitch out the full screen process. How could I make it so if they tap during the animation process it will just cancel the animation and do their desired action as intended? Could I be doing this process better?

share|improve this question
4  
"Long story short," as the first words of a very long question... ;) –  dasdom Aug 2 '13 at 16:21
2  
You could try and disable the user interaction of the navigation bar and its buttons and self.view during the 0.35 seconds of animation. –  Anil Aug 2 '13 at 16:25
    
dasdom, you're right, I meant that as a summary of the question, not as the whole question. :) –  Doug Smith Aug 6 '13 at 16:19
1  
I agree with @Anil. I don't think it's ugly to use self.navigationController.navigationBar.userInteractionEnabled = NO; if you want to block user interactions on that view during the animation. –  nst Aug 6 '13 at 16:35
1  
ah, sorry. you are right. rather than disabling user interaction, make the navbar pass through user interaction by creating your own UINavigationBar subclass and override hitTest:withEvent: so the navbar ignores touches while it is invisble. Your views below the navbar should still receive these touches. –  XJones Aug 6 '13 at 23:37

8 Answers 8

up vote 1 down vote accepted
+200

I think you can do this without adjusting any frames or constraints using these principles:

1) Make the window's background color the same as your view's

2) Add a tap gesture recognizer to the window. This allows tapping anywhere on the screen (except the status bar when its alpha isn't 0) whether the navigation bar is visible or not. This allows you to not have to set the navigation bar to hidden which would cause your view to resize.

3) Use hitTest: in the tapper's action method to check if the user tapped the navigation bar, and don't fade out if the tap was there.

4) Use UIViewAnimationOptionBeginFromCurrentState and UIViewAnimationOptionAllowUserInteraction in the animation block so the fade-in or fade-out can be reversed smoothly with another touch.

5) Enclose all the bottom controls in a clear UIView so you can just fade out that UIView instead of all the individual controls.

Here is the code that worked:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.view.window.backgroundColor = self.view.backgroundColor;
    UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fadeInFadeOut:)];
    [self.view.window addGestureRecognizer:tapper];

}


-(void)fadeInFadeOut:(UITapGestureRecognizer *)sender {
    static BOOL hide = YES;
    id hitView = [self.navigationController.view hitTest:[sender locationInView:self.navigationController.view] withEvent:nil];

    if (! [hitView isKindOfClass:[UINavigationBar class]] && hide == YES) {
        hide = ! hide;
        [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
        [UIView animateWithDuration:.35 delay:0 options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:^{
            self.navigationController.navigationBar.alpha = 0;
            self.bottomView.alpha = 0;
        } completion:nil];

    }else if (hide == NO){
        hide = ! hide;
        [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
        [UIView animateWithDuration:.35 delay:0 options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:^{
            self.navigationController.navigationBar.alpha = 1;
            self.bottomView.alpha = 1;
        } completion:nil];
    }
}
share|improve this answer
    
I already have a tap gesture on self.view (as shown in the video) but if I change it to self.view.window the @selector method is never called. –  Doug Smith Aug 7 '13 at 23:41
    
@DougSmith, did you change the selector argument to match the name of my method? –  rdelmar Aug 7 '13 at 23:45
    
No, I left it as the method I had previously to see if it would be called before I tried anything else. It's given a valid method (I can command-click it to jump to it) but the method is never called. –  Doug Smith Aug 7 '13 at 23:47
    
Here's my code: gist.github.com/anonymous/6180075 –  Doug Smith Aug 7 '13 at 23:49
    
@DougSmith, I don't know what to tell you. The code I posted here is tested, and it works. –  rdelmar Aug 7 '13 at 23:49

Why not use the animationCompleted delegate or block?

share|improve this answer
    
Because of the order things are firing at. I only want the nav bar to be there after the animation finishes if it's being set to hidden (that's the only place I use the delay). Otherwise I need it to be "there" immediately so its frame is there, but let the animation change the alpha back. –  Doug Smith Aug 6 '13 at 16:18
    
Can't you do the if-true bit in the completion block, and the if-false bit directly? Just a thought. –  Rob van der Veer Aug 6 '13 at 19:18
    
I can, but this doesn't help the root of the problem: the undo causes odd behaviour where it glitches out. I've attached a video of it in the OP. –  Doug Smith Aug 6 '13 at 22:32

The way I'd do this is simply create a BOOL flag that I'd call something like isTransitioning, such that once the hiding/unhiding process starts, the hideControls method returns immediately if a transition is in progress. That way you're not messing with touch events; you're directly stopping the unwanted glitches without causing side-effects elsewhere (you'll need to declare isTransitioning as a property/ivar outside of the method, obviously):

- (void)hideControls:(BOOL)hidden {

    //Check there isn't a hide/unhide already in progress:
    if(self.isTransitioning == YES) return;

    //if there wasn't already a transition in progress, set
    //isTransitioning to YES and off we go:
    self.isTransitioning = YES;

    self.navigationController.view.backgroundColor = self.view.backgroundColor;
    int statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;

    [UIView animateWithDuration:0.35 animations:^{
        [[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];

        if (hidden) {
            self.navigationController.navigationBar.alpha = 0.0;
            self.instructionsLabel.alpha = 0.0;
            self.backFiftyWordsButton.alpha = 0.0;
            self.forwardFiftyWordsButton.alpha = 0.0;
            self.WPMLabel.alpha = 0.0;
            self.timeRemainingLabel.alpha = 0.0;
        }
        else {
            self.navigationController.navigationBar.alpha = 1.0;
            self.instructionsLabel.alpha = 1.0;
            self.backFiftyWordsButton.alpha = 1.0;
            self.forwardFiftyWordsButton.alpha = 1.0;
            self.WPMLabel.alpha = 1.0;
            self.timeRemainingLabel.alpha = 1.0;
        }

        [self.view layoutIfNeeded];
    }];

    // Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
    if (hidden) {
        double delayInSeconds = 0.35;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
            self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;

        //Unset isTransitioning now we're finished:
        self.isTransitioning = NO;
        });
    }
    else {
        [self.navigationController setNavigationBarHidden:NO animated:NO];
        self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;

        //Unset isTransitioning now we're finished:
        self.isTransitioning = NO;
    }
}

I haven't directly tested this code, but you can see what I'm getting at, I'm sure.

share|improve this answer
    
If I just return, isn't that basically the equivalent of stopping user interaction for the 0.35 seconds? That's really not optimal given the context this is being used in. –  Doug Smith Aug 6 '13 at 18:20
    
It blocks cancellation of the fade by tapping again, but not any other user interaction. Try doing the same in iBooks - tap to enter full-screen mode and tap again during the transition. The second tap will be ignored while the transition is happening. It's the way almost all interfaces of this type behave; users won't notice. –  davidf2281 Aug 6 '13 at 18:53
    
Hmm, as iBooks is exactly what I'm going for interaction-wise, this is very interesting. Thank you. –  Doug Smith Aug 6 '13 at 19:15
    
It seems iBooks does not do this. I can repeatedly tap the nav bar (between the Chapters and the AA button) and it will toggle as fast as I can tap, no interaction being disabled whatsoever. With this result, it does stop user interaction, which I unfortunately don't want. –  Doug Smith Aug 6 '13 at 22:31

The other answers are helpful, but one thing you should probably do is instead of hard-coding your animation duration to 0.35, try using UINavigationControllerHideShowBarDuration. This will make your app more resilient to changes to UIKit behavior.

share|improve this answer
    
That enum is shorter than the length of time it takes the status bar to fade out, which doesn't have an enum, and is around 0.35. It messes up the animation's flow if I use a value that low. –  Doug Smith Aug 6 '13 at 18:11

EDIT #2

looking at the docs, hitTest:withEvent: simply calls pointTest:withEvent: on all subviews of the view. It clearly states that views with alpha level less than 0.01 are ignored. I think we are on the right path here, just need to explore further. I'm sure there's a way to have a view with alpha == 0.0f pass through touches to any views beneath it. Hopefully you (or someone else here) will get it. If I have time I'll dive into some code and try to help further.

EDIT #1: try overriding pointInside:withEvent:

Sorry for the stream of consciousness answer here. Normally I would either test this myself or paste code from a production app but I'm too busy right now.

I think overriding pointInside:withEvent will work:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.alpha == 0.0f) {
        return NO;
    }
    else {
        return [super pointInside:point withEvent:event];
    }
}

ORIGINAL ANSWER: I would try subclassing UINavigationBar and overriding hitTest:withEvent: so that the navigation bar ignores touches while it is invisible. I didn't test this but should be something like:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // if alpha == 0.0f, the nav bar should ignore touches
    if (self.alpha == 0.0f) {
        return nil;
    }

    // else hitTest as normal
    else {
        return [super hitTest:point withEvent:event];
    }
}

If testing for alpha == 0.0f isn't how you want to decide to ignore touches, you could also add your own property and do it that way.

@property (nonatomic) BOOL ignoreTouches;

In your hitTest:withEvent: implementation check for if (ignoreTouches) and return nil.

share|improve this answer
    
That overriding method gets called, and it will return nil when the nav bar is tapped and its alpha is 0, but it doesn't pass the touch to the subview. It just doesn't register the touch at all. –  Doug Smith Aug 7 '13 at 0:20
    
Argh! So close! It still doesn't seem to call pointInside: if the nav bar's alpha is 0.0. :/ –  Doug Smith Aug 7 '13 at 0:54

To turn off receiving touch events while you animate the UINavigationBar then subclass it and add the following method:

-(BOOL) canBecomeFirstResponder{
    NSLog(@"Decide if to allow the tap through");
    if (self.hiding) return NO;
    return YES;
}

Then you can control if and when you allow touches to be responded to.

Keep in mind that UINavigationBar is a subclass of UIResponder which allows you to override this and many other methods. I also often forget to look up the inherit chain.

share|improve this answer

You could use animateWithDuration:animations:completion:. The completion block is executed after the animations have completed (source). It also has an added benefit; if you decide to change the timing of the animations in the future, you won't have to worry changing the timing in two places.

[UIView animateWithDuration:0.35 animations:^{
    [[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];

    if (hidden) {
        self.navigationController.navigationBar.alpha = 0.0;
        self.instructionsLabel.alpha = 0.0;
        self.backFiftyWordsButton.alpha = 0.0;
        self.forwardFiftyWordsButton.alpha = 0.0;
        self.WPMLabel.alpha = 0.0;
        self.timeRemainingLabel.alpha = 0.0;
    }
    else {
        self.navigationController.navigationBar.alpha = 1.0;
        self.instructionsLabel.alpha = 1.0;
        self.backFiftyWordsButton.alpha = 1.0;
        self.forwardFiftyWordsButton.alpha = 1.0;
        self.WPMLabel.alpha = 1.0;
        self.timeRemainingLabel.alpha = 1.0;
    }

    [self.view layoutIfNeeded];
}
completion:^(BOOL finished){
    // Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
    if ( finished ) {
        if (hidden) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
            self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;
        }
        else {
            [self.navigationController setNavigationBarHidden:NO animated:NO];
            self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;
        }
    }
}];

Based on the comments, you could disable the navigation bar before the animations start, and re-enable in the completion block, but that is up to you test what works best for you..

share|improve this answer
    
This is very close, but unfortunately it ends up looking like this: cl.ly/0k0A1d3V462V (video) –  Doug Smith Aug 6 '13 at 22:58
    
That's because there's no navigation bar to change the alpha of in the animation block as it was hidden from before. You mentioned I could change it before, but when I try that, as the status bar is missing it causes the nav bar to appear at the very top of the screen, then when the status bar does show up, it shows over it. Also it messes with auto layout constraints and bounces the text around. So if I change the status bar to be enabled at the beginning as well, because of Auto Layout constraints it still bounces around due to the height of the view changing. –  Doug Smith Aug 6 '13 at 23:00

Rather than messing with views in the nav-controller stack, why not have a FullScreenViewController (from UIViewController) and make that the root of your app. Then add the NavController (and its stack) on top of that. When the time comes, just fade the whole NavController, exposing your FullScreenViewController.

(You could do this idea upside down, too -- something like this (typed in browser -- lots of syntax errors!):

UIViewController *vc = // ... FullScreenViewController
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[navController presentViewController: vc animated: YES completion: nil];

NOTE: you can also make use of childViewControllers to have an abstract class that contains the VC which will be both the full-screen and the not-full screen versions, and then just steal its view, as desired.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.