When I navigate through UIPageViewController
faster than its transition animation I am getting \'Unbalanced calls to begin/end appearance transitions for <
My solution in swift, simple and working:
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
}
}
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;
}
I will try to ignore gesture on UIPageViewControllers while transitioning.
I had to add it to viewDidAppear: to make it work
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.pageAnimationFinished = YES;
}
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
}
}
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.