UIPageViewController 手势正在调用 viewControllerAfter: 但没有动画
Posted
技术标签:
【中文标题】UIPageViewController 手势正在调用 viewControllerAfter: 但没有动画【英文标题】:UIPageViewController gesture is calling viewControllerAfter: but doesn't animate 【发布时间】:2012-09-21 15:08:44 【问题描述】:我有一个关于 UIPageViewController 的非常有趣的问题。
我的项目的设置与示例基于页面的应用程序模板非常相似。
时不时地(但在一定程度上可重现)某个平移手势会调用-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
。
我返回下一页的视图控制器,但从未运行翻页动画,也从未调用我的委托方法。
这是viewControllerAfterViewController
的代码
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
PageDisplayViewController *vc = (PageDisplayViewController *)viewController;
NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
if(index == (self.pageFetchController.fetchedObjects.count - 1)) return nil;
return [self getViewControllerForIndex:(++index)];
这里是getViewControllerForIndex:
-(PageDisplayViewController *)getViewControllerForIndex:(NSUInteger)index
PageDisplayViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"PageDisplayController"];
newVC.page = [self.pageFetchController.fetchedObjects objectAtIndex:(index)];
newVC.view.frame = CGRectMake(0, 0, 1024, 604);
NSLog(@"%i", index);
if(index == 0)
//We're moving to the first, animate the back button to be hidden.
[UIView animateWithDuration:0.5 animations:^
self.backButton.alpha = 0.f;
completion:^(BOOL finished)
self.backButton.hidden = YES;
];
else if(index == (self.pageFetchController.fetchedObjects.count - 1))
[UIView animateWithDuration:0.5 animations:^
self.nextButton.alpha = 0.f;
completion:^(BOOL finished)
self.nextButton.hidden = YES;
];
else
BOOL eitherIsHidden = self.nextButton.hidden || self.backButton.hidden;
if(eitherIsHidden)
[UIView animateWithDuration:0.5 animations:^
if(self.nextButton.hidden)
self.nextButton.hidden = NO;
self.nextButton.alpha = 1.f;
if(self.backButton.hidden)
self.backButton.hidden = NO;
self.backButton.alpha = 1.f;
];
return newVC;
基本上,我创建视图控制器,设置它的数据对象,然后根据索引淡出下一步/后退按钮。
委托方法
-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
PageDisplayViewController *vc = [previousViewControllers lastObject];
NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
if (!completed)
[self.pagePreviewView setCurrentIndex:index];
NSLog(@"Animation Did not complete, reverting pagepreview");
else
PageDisplayViewController *curr = [pageViewController.viewControllers lastObject];
NSUInteger i = [self.pageFetchController.fetchedObjects indexOfObject:curr.page];
[self.pagePreviewView setCurrentIndex:i];
NSLog(@"Animation compeleted, updating pagepreview. Index: %u", i);
我只是注意到这个问题,因为我的后退按钮会随机重新出现在屏幕上。在其中折腾了一些NSLog()
语句后,我注意到我的dataSource 方法被调用,索引为1,但没有播放动画或调用委托。更可怕的是,如果我尝试平移下一页,索引 1 会再次调用。
我担心这可能是 UIPageViewController 的错误。
【问题讨论】:
啊,忘了补充,在我的情况下,垂直平移是导致这种行为的原因。 嗨,你是怎么解决这个问题的? 【参考方案1】:由于我在第一个答案中仍然收到神秘的实现崩溃,因此我一直在寻找一个“足够好”的解决方案,该解决方案较少依赖于个人对页面视图控制器 (PVC) 底层行为的假设。这是我设法想出的。
我以前的方法有点侵入性,与其说是可接受的解决方案,不如说是一种解决方法。与其与 PVC 抗争,强迫它做我认为应该做的事,不如接受以下事实:
pageViewController:viewControllerBeforeViewController:
和 pageViewController:viewControllerAfterViewController:
方法可以被 UIKit 调用任意次数,并且
绝对不能保证其中任何一个都对应于分页动画,也不保证它们之后会调用pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
这意味着我们不能将 before/after 方法用作“animation-begin”(但请注意,didFinishAnimating
仍用作“animation-end”事件)。那么我们如何知道动画确实已经开始了呢?
根据我们的需要,我们可能对以下活动感兴趣:
用户开始摆弄页面: 一个很好的指标是之前/之后的回调,或者更准确地说是它们中的第一个。
翻页手势的第一个视觉反馈:
我们可以在 PVC 的点击和平移手势识别器的 state
属性上使用 KVO。当观察到UIGestureRecognizerStateBegan
值进行平移时,我们可以非常确定视觉反馈会随之而来。
用户通过释放触摸完成拖动页面:
再次,KVO。当UIGestureRecognizerStateRecognized
值被报告为平移或点击时,是PVC实际要翻页的时候,所以这可以用作“动画开始”。
UIKit 启动分页动画: 我不知道如何获得对此的直接反馈。
UIKit 结束分页动画:
小菜一碟,听pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
。
对于 KVO,只需抓取 PVC 的手势识别器,如下所示:
@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;
然后将您的班级注册为state
属性的观察者:
[pvcPanGestureRecognizer addObserver:self
forKeyPath:@"state"
options:NSKeyValueObservingOptionNew
context:NULL];
[pvcTapGestureRecognizer addObserver:self
forKeyPath:@"state"
options:NSKeyValueObservingOptionNew
context:NULL];
并实现通常的回调:
- (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;
// ...
完成后,不要忘记取消订阅这些通知,否则您的应用可能会出现泄漏和奇怪的崩溃:
[pvcPanGestureRecognizer removeObserver:self
forKeyPath:@"state"];
[pvcTapGestureRecognizer removeObserver:self
forKeyPath:@"state"];
就是这样!
【讨论】:
【参考方案2】:请首先查看我的其他答案,这个答案有严重缺陷,但我将其留在这里,因为它可能仍然对某人有所帮助。
首先,免责声明:以下解决方案是HACK。它确实在我测试的环境中工作,但不能保证它在你的环境中工作,也不能保证它不会被下一次更新破坏。所以请谨慎行事。
TL;DR: 抓住UIPageViewController
的UIPanGestureRecognizer
并劫持其委托调用,但继续将它们转发到原始目标。
加长版:
我对这个问题的发现:ios 6 中提供的 UIPageViewController
与 iOS 5 中的行为不同,因为它可能在其数据源上调用 pageViewController:viewControllerBeforeViewController:
,即使任何页面都没有打开感觉(阅读:没有识别到点击、滑动或有效方向匹配的平移)。当然,这打破了我们之前的假设,即 before/after 调用等同于“动画开始”触发器,并且始终遵循对代理的 pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
调用。 (最终这是一个大胆的假设,但我想我并不孤单。)
我发现当页面视图控制器上的默认UIPanGestureRecognizer
开始识别最终与控制器方向不匹配的平移手势(例如垂直在水平寻呼 PVC 中平移)。有趣的是,在我的环境中,受到打击的总是“之前”方法,而不是“之后”。 Others suggested 干扰了手势识别器的委托,但这对我来说并没有像那里描述的那样工作,所以我一直在试验。
最后我找到了解决方法。首先我们抓取页面视图控制器的平移手势识别器:
@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;
然后我们在我们的类中实现UIGestureRecognizerDelegate
协议:
- (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;
显然,这些方法没有做任何明智的事情,它们只是将调用转发给原始委托(确保它实际实现了它们)。尽管如此,这种转发似乎足以让 PVC 正常运行,并且在不需要时不调用数据源。
此解决方法为我解决了运行 iOS 6 的设备上的问题。使用 iOS 6 SDK 编译但部署目标为 iOS 5 的代码已经在 5.x 设备上完美运行,因此无需修复但根据我的测试,它也不会造成任何伤害。
希望有人觉得这很有用。
【讨论】:
请注意,我刚刚检测到一些可能与此解决方法有关的奇怪崩溃。也就是说,如果您的页面数量有限,并且您尝试从最后一页开始滑动,那么应用程序会崩溃,说您没有提供必要数量的视图控制器,即使您有意返回 @ 987654331@ 表示没有了。一旦我找到解决办法,我会在这里发布。 Lvsti,我遇到了同样的错误。我知道这是很久以前的事了,但你找到解决方案了吗?【参考方案3】:我已经尝试了您的解决方案,它几乎可以正常工作,但仍然存在一些问题。最好的解决方案是添加方法
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
从 iOS 6 开始可用,并且它是必需的。如果不实施它,这些手势可能会出现问题。实施它有助于解决大部分问题。
【讨论】:
我正在查看的 iOS 7 文档将其显示为可选。【参考方案4】:试试这个……
- (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;
....
【讨论】:
以上是关于UIPageViewController 手势正在调用 viewControllerAfter: 但没有动画的主要内容,如果未能解决你的问题,请参考以下文章
UIPageViewController 手势覆盖我的 UITableView
iOS 7 iPad:UIPageViewController 中的 UITableView 滑动删除(手势识别器冲突)
如何设置 UIPageViewController(分页类型)手势委托