Basem Saadawy
Basem Saadawy

Reputation: 1818

UIPageViewController transition 'Unbalanced calls to begin/end appearance transitions for '

When I navigate through UIPageViewController faster than its transition animation I am getting 'Unbalanced calls to begin/end appearance transitions for <MyDataViewController>' and one of the two views in landscape isn't shown until I try to turn the page.

Anybody has an idea to solve this bug?

Upvotes: 19

Views: 12663

Answers (10)

buxik
buxik

Reputation: 2635

My solution in swift, simple and working:

  1. Set pageviewcontroller delegate to your class
  2. Add below code

    extension MyPageVC: UIPageViewControllerDelegate {
    
        func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
            self.view.isUserInteractionEnabled = false
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            self.view.isUserInteractionEnabled = true
        }
    }
    

Upvotes: 3

Mr Stanev
Mr Stanev

Reputation: 1698

Here's the Swift version of Bill Cheswick's answer (currently the top answer):

Add a variable to hold the current state:

var pageIsAnimating = false

Set the animating state:

func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
    self.pageIsAnimating = true
}

func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if finished || completed {
        self.pageIsAnimating = false
    }
}

Block the transitions if it's currently animating:

func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    if self.pageIsAnimating {
        return nil
    }

    // Your code here
}

func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    if self.pageIsAnimating {
        return nil
    }

    // Your code here
}

Thank you Bill Cheswick!

Upvotes: 4

Oxcug
Oxcug

Reputation: 6604

Here's a QUICK version using the delegate:

add this code (make sure you're including the UIPageViewControllerDelegate in your header or class extension, and assign self.pageViewController.delegate = self;):

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
   self.pageAnimationFinished = NO;
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
    self.pageAnimationFinished = YES;
}

then check self.pageAnimationFinished and return nil if it's == NO.

Longer Explanation:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed

We can use this delegate method from UIPageViewControllerDelegate to know when the animation from flipping or swiping through pages finishes. Using this we just can implement it like this:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
    pageAnimationFinished = YES;
}

then, just return nil in your

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(PageViewController *)viewController

and

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(PageViewController *)viewController

when

pageAnimationFinished == NO. Be sure to set pageAnimationFinished to NO when you animate. The best way to know when you animate is by using the opposite of

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed

namely:

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers

I haven't seen that warning ever since and this can be done in 1/3 of the lines as the other solutions. And it's MUCH easier to follow.

Upvotes: 6

Leo Thiessen
Leo Thiessen

Reputation: 31

How about this:

- (void)pageViewController:(UIPageViewController*)pgVC willTransitionToViewControllers:(NSArray*)pendingVCs
{
    pgVC.dataSource = nil; // ... to disallow user to change pages until animation completes
}

- (void)pageViewController:(UIPageViewController*)pgVC
        didFinishAnimating:(BOOL)finished
   previousViewControllers:(NSArray*)prevVCs
       transitionCompleted:(BOOL)completed
{
    if(completed || finished) {
        pgVC.dataSource = _pgViewDataSource; // ... to allow user to change pages again
    }
}

Upvotes: 1

I had to add it to viewDidAppear: to make it work

    - (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.pageAnimationFinished = YES;
}

Upvotes: 0

Dmytro Strelbytskyi
Dmytro Strelbytskyi

Reputation: 71

Good answer from Basem Saadawy but it has some defect.

Actually the delegate's gestureRecognizerShouldBegin: could be called with no further animation started. This is possible if you start your gesture by vertical finger's moving and its horizontal offset is not enough to start the animation (but is enough to launch gestureRecognizerShouldBegin:). Thus our variable pageAnimationFinished will be set to NO without an actual animation. Therefore the pageViewController: didFinishAnimating: will never be called and you get the current page frozen without a possibility to change it.

That's why a better place to assign NO to this variable is a gesture recognizer's action method with examination of its velocity and translation (we are interested in horizontal direction only).

So the final steps are:

1) Declare an instance variable (a flag):

BOOL pageAnimationFinished;

2) Set its initial value

- (void)viewDidLoad
{
    [super viewDidLoad];
    ...
    pageAnimationFinished = YES;
}

3) Assign a delegate and a custom action to the pan gesture recognizers

for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
    if ([gesRecog isKindOfClass:[UIPanGestureRecognizer class]])
    {
        gesRecog.delegate = self;
        [gr addTarget:self action:@selector(handlePan:)];
    }
}

3') Animation is really started when the gesture's translation is greater in horizontal direction and the finger is moving horizontally at a moment.
I guess the same logic is used in the internal recognizer's action assigned by UIPageViewController.

- (void) handlePan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (pageAnimationFinished && gestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        CGPoint vel = [gestureRecognizer velocityInView:self.view];
        CGPoint tr = [gestureRecognizer translationInView:self.view];
        if (ABS(vel.x) > ABS(vel.y) && ABS(tr.x) > ABS(tr.y))
            pageAnimationFinished = NO; // correct place
    }
}

4) Disallowing a gesture if an animation is not finished.

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ([gestureRecognizer.view isEqual:self.view] || [gestureRecognizer.view isEqual:self.pageViewController.view]))
    {
        UIPanGestureRecognizer * panGes = (UIPanGestureRecognizer *)gestureRecognizer;
        if(!pageAnimationFinished || (currentPage < minimumPage && [panGes velocityInView:self.view].x < 0) || (currentPage > maximumPage && [panGes velocityInView:self.view].x > 0))
            return NO;
    }
    return YES;
}

5) Animation is finished

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    pageAnimationFinished = YES;
}

I played too much with it and seems this is a nice solution that works well.

Upvotes: 6

Bill Cheswick
Bill Cheswick

Reputation: 643

The above answers were right, but I think more elaborate than needed, and cookbook is helpful. So here is what seems to be working for me:

In the view controller that sets up and calls the pageViewController, declare:

@property (assign)              BOOL pageIsAnimating;

and in viewDidLoad:

    pageIsAnimating = NO;

add this:

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
    pageIsAnimating = YES;
}

and add a couple of lines to:

- (void)pageViewController:(UIPageViewController *)pageViewController
    didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers
   transitionCompleted:(BOOL)completed {
    if (completed || finished)   // Turn is either finished or aborted
        pageIsAnimating = NO;
    ...
}

The gestures are suppressed by declining to provide view controller information:

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
   viewControllerAfterViewController:(UIViewController *)viewController {
    if (pageIsAnimating)
        return nil;
    ...
    return after;
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
  viewControllerBeforeViewController:(UIViewController *)viewController {
    if (pageIsAnimating)
        return nil;
    ...
    return before;
}

Oh, and orientation changes reset the flag:

- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
               spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation {
    pageIsAnimating = NO;
    ...
}

Upvotes: 29

Basem Saadawy
Basem Saadawy

Reputation: 1818

Solved following these steps:
1- Declare a flag to indicate that the animation has finished or not:

BOOL pageAnimationFinished;

2- Set this flag to true in viewDidLoad:

pageAnimationFinished = YES;

3- Disable tapGesture for the pageViewController and assign 'self' to panGestureRecognizer delegate:

for (UIGestureRecognizer * gesRecog in self.pageViewController.gestureRecognizers)
{
    if ([gesRecog isKindOfClass:[UITapGestureRecognizer class]])
        gesRecog.enabled = NO;
    else if ([gesRecog isKindOfClass:[UIPanGestureRecognizer class]])
        gesRecog.delegate = self;
}

4- Allow/Disallow panGestureRecognizer through the following gesture recognizer delegate method:

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && ([gestureRecognizer.view isEqual:self.view] || [gestureRecognizer.view isEqual:self.pageViewController.view]))
    {
        UIPanGestureRecognizer * panGes = (UIPanGestureRecognizer *)gestureRecognizer;
        if(!pageAnimationFinished || (currentPage < minimumPage && [panGes velocityInView:self.view].x < 0) || (currentPage > maximumPage && [panGes velocityInView:self.view].x > 0))
            return NO;
        pageAnimationFinished = NO;
    }
    return YES;
}

5- Add the following pageViewController delegate method:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    pageAnimationFinished = YES;
}

Upvotes: 9

timzilla
timzilla

Reputation: 94

Make use of your UIPageViewControllerDelegate methods and set up guards to prevent creating new page views when excessive page turns are detected.

  1. You can disable gesture recognizers
  2. Set "userInteraction" to disabled on the UIView
  3. maintain a flag on the UIPageViewController to ignore further input when there is an animation occuring. (warning about this option.. ios5 and ios6 have different ways of determining when the animation started..)

Upvotes: 0

Basem Saadawy
Basem Saadawy

Reputation: 1818

I will try to ignore gesture on UIPageViewControllers while transitioning.

Upvotes: -2

Related Questions