19

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?

Nayan
  • 3,014
  • 2
  • 17
  • 33
Basem Saadawy
  • 1,808
  • 2
  • 20
  • 30
  • Please post your viewWill/DidAppear methods. – Martol1ni Nov 06 '12 at 10:13
  • - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.pageNumber = self.pageNumber; self.pageHTML = self.pageHTML; } Where pageHTML loads an html string into a UIWebView which is a subview. – Basem Saadawy Nov 06 '12 at 10:58
  • 2
    Looks good. Most "Unbalanced calls", are because a new ViewController is being pushed or presented before the previous done is done initializing. – Martol1ni Nov 06 '12 at 11:08

10 Answers10

29

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;
    ...
}
Bill Cheswick
  • 634
  • 7
  • 12
9

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;
}
Basem Saadawy
  • 1,808
  • 2
  • 20
  • 30
  • You shouldn't change the delegate for this UIPageViewController gestures, as they rely on private implementations to work properly. – Elland May 29 '13 at 09:48
  • You should be using the delegate. The best solution is here http://stackoverflow.com/a/22869135/951349 . – SmileBot Oct 18 '15 at 01:36
  • #smileBot my application supports iOS 5 and (pageViewController:willTransitionToViewControllers:) available only from iOS 6. – Basem Saadawy Oct 18 '15 at 11:59
6

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.

Community
  • 1
  • 1
  • Thanks in advance. The issue you mentioned doesn't appear for me because there is no UIPanGestureRecognizer attached to the view that holding the UIPageViewController. Any way, your suggestion is a good modification. – Basem Saadawy May 07 '13 at 12:14
6

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.

Community
  • 1
  • 1
Oxcug
  • 6,524
  • 2
  • 31
  • 46
  • 1
    Nice answer. Should be the accepted one IMO. I've edited your answer a bit. IMO your property pageAnimationFinished would be better as animating with the getter set to isAnimating. This way you check if your index is 0 OR self.isAnimating and return nil. It's a style thing. :) – SmileBot Oct 18 '15 at 01:47
4

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!

Mr Stanev
  • 1,662
  • 1
  • 19
  • 26
  • 1
    I can confirm that this works for me, but only on gesture driven page transitions. A side effect of this solution is that the user unfortunately can't just thumb through all the views as quickly as their finger can swipe / tap on the edges.. Also, when programmatically changing pages using pageViewController.setViewControllers(), the function for setting pageIsAnimating aren't called. But this can be mitigated by setting pageIsAnimating to false in the completion block of pageViewController.setViewControllers() – SomeGuy Nov 18 '18 at 05:12
3

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
        }
    }
    
buxik
  • 2,583
  • 24
  • 31
1

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
    }
}
0

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..)
timzilla
  • 84
  • 6
  • can you tell me to fix that problem ?I am facing this problem but no solution. – Sugan S Jan 11 '13 at 05:40
  • there are many options you have, but basically you are seeing this error if the page turn is being invoked before the animation is complete (like swiping rapidly). so what you want to do is set up your code to block/ignore further swipes until the current animation is complete. i resolved it on my end by having a flag set during animation and further calls to page turn would be simply ignored until the animation completed and the flag was cleared. this also serves to throttle excessive page turn requests and is easier on the OS/memory. – timzilla Jan 11 '13 at 21:04
  • in contentview controller viewwillappear method am checking flag value to appear but nothing happend still am getting empty view.can u help me? – Sugan S Jan 15 '13 at 04:30
0

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

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

I will try to ignore gesture on UIPageViewControllers while transitioning.

Basem Saadawy
  • 1,808
  • 2
  • 20
  • 30
  • Did you manage to fix the problem? Can you post the solution? – Viachaslau Tysianchuk Nov 21 '12 at 15:33
  • @Basem did you fixed the problem can you tell me way to fix it?Me also facing the same problem still no solution? – Sugan S Jan 11 '13 at 05:34
  • @BasemSaadawy i have pageviewcontroller and contentviewcontroller where i have to restrict view to be loaded? – Sugan S Jan 15 '13 at 04:17
  • or can u add some code am trying in viewdidload and viewdidiunload but not solved @BasemSaadawy – Sugan S Jan 15 '13 at 04:18
  • And am using `UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController` method. – Sugan S Jan 15 '13 at 04:39
  • @sugan.s Did you try the solution above? – Basem Saadawy Jan 15 '13 at 14:44
  • I set the flag but not working am using default delegate for swipe like UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController and afterviewcontroler – Sugan S Jan 16 '13 at 04:22
  • There is no swipe gesture in the pageViewController. The only gestures attached to it are tap and pan. – Basem Saadawy Jan 16 '13 at 09:09