Interactivepopgesturerecognizer Corrupts Navigation Stack on Root View Controller

interactivePopGestureRecognizer pop to root instead of 1 top controller

I found a way to just delete previous viewController from stack after pushing a new one.

navigationController.pushViewController(newViewController, animated: true, completion: {
self.navigationController.removePreviousViewController()
})

A here is an extension

extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) {
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}

func removePreviousViewController() {
if viewControllers.count > 2 {
viewControllers.removePrevious()
}
}
}

Some helpers

extension Array {
mutating func removePrevious() {
remove(at: count - 2)
}
}

iOS 7, corrupt UINavigationBar when swiping back fast using the default interactivePopGestureRecognizer

TL;DR

I made a category on UIViewController that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.

The Problem, and the Solution

I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.

You can hook into these events using a couple methods on the transitionCoordinator, specifically notifyWhenInteractionEndsUsingBlock: and animateAlongsideTransition:completion:. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.

Summary

  1. Listen for when the interactive portion of the transition ends
  2. Gather up the animations for all the views' layers in the navigation bar
  3. Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
  4. Listen for when the non-interactive portion of the transition ends
  5. Apply the copied/modified animations back to the views' layers

Code

And here's the code for the UIViewController category:

@interface UIViewController (FixNavigationBarCorruption)

- (void)fixNavigationBarCorruption;

@end

@implementation UIViewController (FixNavigationBarCorruption)

/**
* Fixes a problem where the navigation bar sometimes becomes corrupt
* when transitioning using an interactive transition.
*
* Call this method in your view controller's viewWillAppear: method
*/
- (void)fixNavigationBarCorruption
{
// Get our transition coordinator
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;

// If we have a transition coordinator and it was initially interactive when it started,
// we can attempt to fix the issue with the nav bar corruption.
if ([coordinator initiallyInteractive]) {

// Use a map table so we can map from each view to its animations
NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory
capacity:0];

// This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {

// Loop through our nav controller's nav bar's subviews
for (UIView *view in self.navigationController.navigationBar.subviews) {

NSArray *animationKeys = view.layer.animationKeys;
NSMutableArray *anims = [NSMutableArray array];

// Gather this view's animations
for (NSString *animationKey in animationKeys) {
CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];

// In case any other kind of animation somehow gets added to this view, don't bother with it
if ([anim isKindOfClass:[CABasicAnimation class]]) {

// Make a pseudo-hard copy of each animation.
// We have to make a copy because we cannot modify an existing animation.
CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];

// CABasicAnimation properties
// Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.byValue = anim.byValue;

// CAPropertyAnimation properties
animCopy.additive = anim.additive;
animCopy.cumulative = anim.cumulative;
animCopy.valueFunction = anim.valueFunction;

// CAAnimation properties
animCopy.timingFunction = anim.timingFunction;
animCopy.delegate = anim.delegate;
animCopy.removedOnCompletion = anim.removedOnCompletion;

// CAMediaTiming properties
animCopy.speed = anim.speed;
animCopy.repeatCount = anim.repeatCount;
animCopy.repeatDuration = anim.repeatDuration;
animCopy.autoreverses = anim.autoreverses;
animCopy.fillMode = anim.fillMode;

// We want our new animations to be instantaneous, so set the duration to zero.
// Also set both the begin time and time offset to 0.
animCopy.duration = 0;
animCopy.beginTime = 0;
animCopy.timeOffset = 0;

[anims addObject:animCopy];
}
}

// Associate the gathered animations with each respective view
[mapTable setObject:anims forKey:view];
}
}];

// The completion block here gets run after the view controller transition animation completes (or fails)
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {

// Iterate over the mapTable's keys (views)
for (UIView *view in mapTable.keyEnumerator) {

// Get the modified animations for this view that we made when the interactive portion of the transition finished
NSArray *anims = [mapTable objectForKey:view];

// ... and add them back to the view's layer
for (CABasicAnimation *anim in anims) {
[view.layer addAnimation:anim forKey:anim.keyPath];
}
}
}];
}
}

@end

And then just call this method in your view controller's viewWillAppear: method (in your test project's case, it would be the ViewController class):

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

[self fixNavigationBarCorruption];
}

Black area with interactivePopGestureRecognizer when popping a view controller with visible nav bar to a one with hidden nav bar

As discussed with HoanNguyen, I had put my code to hide/show the navigation bar on viewWillAppear/Disappear but finally I figured out that the trick was to set the values animated. Weird, but this solved my problem and the black area is now gone:

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:self.shouldHideNavBar animated:animated];
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController setNavigationBarHidden:!self.shouldHideNavBar animated:animated];
}

Can you override the Navigation Controllers 'InteractivePopGestureRecognizer' action?

You could use Touch Events and UISwipeGestureRecognizer to do that.

The workaround that is override TouchesBegan method to detect the started point whether fit your needs, and if so add UISwipeGestureRecognizer for View.

SwipeGestureRecognizer rightSwipeGesture;

public override void TouchesBegan (NSSet touches, UIEvent evt)
{
base.TouchesBegan (touches, evt);
UITouch touch = touches.AnyObject as UITouch;
if (touch != null)
{
//code here to handle touch
CoreGraphics.CGPoint swipPoint = touch.LocationInView(View);
if(swipPoint.X < 0.5)
{
rightSwipeGesture = new SwipeGestureRecognizer { Direction = SwipeDirection.Right };
rightSwipeGesture.Swiped += OnSwiped;
View.AddGestureRecognizers(rightSwipeGesture);
}
}
}

public override void TouchesEnded (NSSet touches, UIEvent evt)
{
base.TouchesBegan (touches, evt);
if(null != rightSwipeGesture ){
rightSwipeGesture.Swiped -= OnSwiped;
View.RemoveGestureRecognizers(rightSwipeGesture);
}
}

=============================Update=================================

I found a workaround only use one GestureRecognizer will make it works. You could have a look at UIScreenEdgePanGestureRecognizer. Although it's a Pan gesture, however if you not deal with somethind with the added view, it will work as a swip gesture. In addition, UIScreenEdgePanGestureRecognizer only can work when on the screen edge. You could set the Left edge to handle your needs.

For example:

UIScreenEdgePanGestureRecognizer panRightGestureRecognizer = new UIScreenEdgePanGestureRecognizer();
panRightGestureRecognizer.Edges = UIRectEdge.Left;
panRightGestureRecognizer.AddTarget(() => HandleSwap(panRightGestureRecognizer));
View.AddGestureRecognizer(panRightGestureRecognizer);

private void HandleSwip(UIScreenEdgePanGestureRecognizer panRightGestureRecognizer)
{
Point point = (Point)panRightGestureRecognizer.TranslationInView(View);
if (panRightGestureRecognizer.State == UGestureRecognizerState.Began)
{
Console.WriteLine("Show slider view");
}
}


Related Topics



Leave a reply



Submit