I have a really interesting issue with UIPageViewController.
My project is set up very similarly to the example Page Based Application template. Every now and then (
Please look at my other answer in the first place, this one has serious flaws but I'm leaving it here as it might still help someone.
First off, a disclaimer: The following solution is a HACK. It does work in the environment I tested but there is no guarantee that it works in yours nor that it won't be broken by the next update. So proceed with care.
TL;DR: grab the UIPanGestureRecognizer
of the UIPageViewController
and hijack its delegate calls but keep forwarding them to the original target.
Longer version:
My findings on the issue: the UIPageViewController
shipped in iOS 6 is different in behavior to the one in iOS 5 in that it may call the pageViewController:viewControllerBeforeViewController:
on its datasource even if there is no page turning going on in any sense (read: no tap, swipe, or valid direction-matching panning has been recognized). This, of course, breaks our former assumption that the before/after calls are equivalent to an "animation begin" trigger and are consistently followed by a pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
call to the delegate. (Eventually this is a bold assumption to make but I guess I was not alone with that.)
I found out that the extra calls to the datasource are likely to happen when the default UIPanGestureRecognizer
on the page view controller starts to recognize a pan gesture that in the end doesn't match the direction of the controller (e.g. vertical panning in a horizontally paging PVC). Interestingly enough, in my environment it was always the "before" method which got hit, never the "after". Others suggested interfereing with the gesture recognizer's delegate but that didn't work for me the way it was described there so I kept experimenting.
Finally I found a workaround. First we grab the pan gesture recognizer of the page view controller:
@interface MyClass () <UIGestureRecognizerDelegate>
{
UIPanGestureRecognizer* pvcPanGestureRecognizer;
id<UIGestureRecognizerDelegate> pvcPanGestureRecognizerDelegate;
}
...
for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
{
if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
{
pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
pvcPanGestureRecognizerDelegate = pvcPanGestureRecognizer.delegate;
pvcPanGestureRecognizer.delegate = self;
break;
}
}
Then we implement the UIGestureRecognizerDelegate
protocol in our class:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
shouldReceiveTouch:touch];
}
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
}
return NO;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizerShouldBegin:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer];
}
return YES;
}
Apparently, the methods don't do anything sensible, they just forward the invocations to the original delegate (making sure that that actually implements them). Still, this forwarding seems to be sufficient for the PVC to behave and not call the datasource when there is no need to.
This workaround fixed the issue for me on devices running iOS 6. Code which was compiled with the iOS 6 SDK but with a deployment target of iOS 5 had already run flawlessly on 5.x devices, so the fix is not necessary there but according to my tests it doesn't do any harm either.
Hope someone finds this useful.
I have tried your solution and it came almost working, but still with some issues. The best solution came with adding method
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
which is available starting from iOS 6 and it is required for it. If not to implement it, issues may occur with those gestures. Implementing it helped to solve major part of issues.
try this...
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
for (UIGestureRecognizer *gr in pageViewController.gestureRecognizers) {
if([gr isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *pgr = (UIPanGestureRecognizer*)gr;
CGPoint velocity = [pgr velocityInView:pageViewController.view];
BOOL verticalSwipe = fabs(velocity.y) > fabs(velocity.x);
if(verticalSwipe)
return nil;
}
}
....
}
Since I was still receiving mysterious crashes with the implementation in my first answer, I kept searching for a "good enough" solution which depends less on personal assumptions about the page view controller's (PVC) underlying behavior. Here is what I managed to come up with.
My former approach was kind of intrusive and was more of a workaround than an acceptable solution. Instead of fighting the PVC to force it to do what I thought it was supposed to do, it seems that it's better accept the facts that:
pageViewController:viewControllerBeforeViewController:
and pageViewController:viewControllerAfterViewController:
methods can be called an arbitrary number of times by UIKit, andpageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
That means we cannot use the before/after methods as "animation-begin" (note, however, that didFinishAnimating
still serves as "animation-end" event). So how do we know an animation has indeed started?
Depending on our needs, we may be interested in the following events:
the user begins fiddling with the page: A good indicator for this is the before/after callbacks, or more precisely the first of them.
first visual feedback of the page turning gesture:
We can use KVO on the state
property of the tap and pan gesture recognizers of the PVC. When a UIGestureRecognizerStateBegan
value is observed for panning, we can be pretty sure that visual feedback will follow.
the user finishes dragging the page around by releasing the touch:
Again, KVO. When the UIGestureRecognizerStateRecognized
value is reported either for panning or tapping, it is when the PVC is actually going to turn the page, so this may be used as "animation-begin".
UIKit starts the paging animation: I have no idea how to get a direct feedback for this.
UIKit concludes the paging animation:
Piece of cake, just listen to pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
.
For KVO, just grab the gesture recognizers of the PVC as below:
@interface MyClass () <UIGestureRecognizerDelegate>
{
UIPanGestureRecognizer* pvcPanGestureRecognizer;
UITapGestureRecognizer* pvcTapGestureRecognizer;
}
...
for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
{
if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
{
pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
}
else if ( [recognizer isKindOfClass:[UITapGestureRecognizer class]] )
{
pvcTapGestureRecognizer = (UITapGestureRecognizer*)recognizer;
}
}
Then register your class as observer for the state
property:
[pvcPanGestureRecognizer addObserver:self
forKeyPath:@"state"
options:NSKeyValueObservingOptionNew
context:NULL];
[pvcTapGestureRecognizer addObserver:self
forKeyPath:@"state"
options:NSKeyValueObservingOptionNew
context:NULL];
And implement the usual callback:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ( [keyPath isEqualToString:@"state"] && (object == pvcPanGestureRecognizer || object == pvcTapGestureRecognizer) )
{
UIGestureRecognizerState state = [[change objectForKey:NSKeyValueChangeNewKey] intValue];
switch (state)
{
case UIGestureRecognizerStateBegan:
// trigger for visual feedback
break;
case UIGestureRecognizerStateRecognized:
// trigger for animation-begin
break;
// ...
}
}
}
When you are done, don't forget to unsubscribe from those notifications, otherwise you may get leaks and strange crashes in your app:
[pvcPanGestureRecognizer removeObserver:self
forKeyPath:@"state"];
[pvcTapGestureRecognizer removeObserver:self
forKeyPath:@"state"];
That's all folks!