ThornTechPublic / InteractiveModal

MIT License
148 stars 37 forks source link

Transition can break when the presenting and presented view controllers have different status bar styles #1

Open tallytarik opened 8 years ago

tallytarik commented 8 years ago

I've just spent hours pulling my hair out trying to figure out why my project was exhibiting a strange bug but your project works fine, and I've finally got it! (but not how to fix it, sadly)

If you use view-controller based status bar styles (UIViewControllerBasedStatusBarAppearance in Info.plist), there's a nasty bug that breaks the transition and leaves the app unusable. To replicate, see the following:

Try to drag down on the modal view a tiny bit very quickly (for example, drag down about 50 pixels only just clicking the mouse for a split second)

In the original project, this works as expected - the modal view snaps back to the top, you can continue to do this (or drag further down to dismiss) and the "close" button continues to function.

Now change the status bar style of one of the view controllers so that they're both different - e.g. in ModalViewController.swift:

override func preferredStatusBarStyle() -> UIStatusBarStyle {
    return UIStatusBarStyle.LightContent
}

Run the app and try the same thing a couple of times, and the transition will break: the modal will snap back to the top but will no longer respond to the gesture recognizer, and the "close" button does not work to dismiss the modal view manually. The status bar will be stuck in the state of the presenting view controller.

This only happens when the status bar is a different style between the two view controllers, but I need this in my app, and I'm not sure how to fix it at all. :(

tallytarik commented 8 years ago

Update: In particular, it seems that something about changing the status bar styles cancels the UIView animation in [DismissAnimator animateTransition:]. The animation seems to be completely cancelled without calling the completion block and therefore [transitionContext completeTransition:] is never called.

I have tried tracking whether or not this completion block is called, and then after a delay calling completeTransition if the block was never called. I'm using Objective-C, so I've got the following code:

__block BOOL didComplete = NO;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    if(!didComplete && [transitionContext transitionWasCancelled]) [transitionContext completeTransition:NO];
});

[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
    fromVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
    didComplete = YES;

    [transitionContext completeTransition:!([transitionContext transitionWasCancelled])];
}];

This works but it's not great - the transition still breaks for a split second before fixing itself. :(

chenr2 commented 8 years ago

@ev0lution Thanks for reporting this issue and including detailed troubleshooting. I'll take a look.

chenr2 commented 8 years ago

@ev0lution I've been banging my head against the wall on this one, and it looks like your approach is the way to go.

I found two radars on this issue:

iOS 8.3 introduced a cross-fade animation when the status bar changes between .Default and .LightContent. As you explained earlier, a very quick pan will trigger cancelInteractiveTransition(). However, since the status bar animation is still in flight, animateWithDuration's completion block never gets called. This means completeTransition isn't called either, and the transition enters an inconsistent state.

According to the radar, this issue is fixed in iOS 9.3ß2. In the meantime, your workaround appears to be the way to go. It captures the transitionContext via closure, and does the cleanup work of calling completeTransition if the animateWithDuration completion block is never called.

Here are some of my failed attempts:

For posterity, here are some of the more helpful links I've found surrounding on this issue:

tallytarik commented 8 years ago

@chenr2 Thanks so much for your time and research on this, super helpful! Good to hear that it's fixed in the latest beta, I'll check it out. :)

nevinjethmalani commented 8 years ago

Is there any update on fixing this? This works very well except for when this issue presents itself.

tallytarik commented 8 years ago

@nevinjethmalani It's actually an iOS issue, and it's fixed in 9.3. You can use my workaround (above) for versions <9.3 if you're okay with the transition being broken for a split second. Another alternative is to set the status bar style manually, before and after the transition/view change, so that the transition isn't trying to animate the status bar style change (which is what seems to break).

nevinjethmalani commented 8 years ago

We have the latest iOS installed on our devices and we are getting the same error. I will try the work around that you posted here. We are using a scrollview instead of a table view to do this maybe that is the reason.

What do you mean by set the status bar style manually? Do we have to create a navigation bar in the storyboard manually? How would we do this?

shawn736 commented 7 years ago

I don't know why, but it is ok for me.You can try in viewWillDisappear to reset status bar style.

nevinjethmalani commented 7 years ago

Do you know if this has been fixed in iOS 10?

alexrmacleod commented 7 years ago

Totally need this so bad in my ios app Tipped, on webviews and collectionviews..... But swift 3 not working :( how to fix

aporohov commented 7 years ago

Working on ios < 9.3


@interface ModalViewController () 

@property (nonatomic, assign) UIStatusBarStyle preferredStatusBarStyle;

@end

@implementation ModalViewController

@synthesize preferredStatusBarStyle;

- (void)viewDidLoad {
    [super viewDidLoad];

    self.preferredStatusBarStyle = UIStatusBarStyleLightContent;
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    self.preferredStatusBarStyle = UIStatusBarStyleDefault;
    [self setNeedsStatusBarAppearanceUpdate];
}

@end