UIPageViewController,如何正确跳转到特定页面,而不会打乱数据源指定的顺序?

64

我发现了一些关于如何使 UIPageViewController 跳转到特定页面的问题,但是我注意到在跳转时会出现一个额外的问题,而且没有任何答案似乎意识到这点。

不想详细介绍我的iOS应用程序(类似于分页日历),以下是我的经验。我声明了一个 UIPageViewController,设置了当前视图控制器,并实现了数据源。

// end of the init method
        pageViewController = [[UIPageViewController alloc] 
        initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
          navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                        options:nil];
        pageViewController.dataSource = self;
        [self jumpToDay:0];
}

//...

- (void)jumpToDay:(NSInteger)day {
        UIViewController *controller = [self dequeuePreviousDayViewControllerWithDaysBack:day];
        [pageViewController setViewControllers:@[controller]
                                    direction:UIPageViewControllerNavigationDirectionForward
                                     animated:YES
                                   completion:nil];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
        NSInteger days = ((THDayViewController *)viewController).daysAgo;
        return [self dequeuePreviousDayViewControllerWithDaysBack:days + 1];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
        NSInteger days = ((THDayViewController *)viewController).daysAgo;
        return [self dequeuePreviousDayViewControllerWithDaysBack:days - 1];
}

- (UIViewController *)dequeuePreviousDayViewControllerWithDaysBack:(NSInteger)days {
        return [[THPreviousDayViewController alloc] initWithDaysAgo:days];
}

编辑注:我为出队方法添加了简化的代码。即使使用这种亵渎的实现,我仍然面临着与页面顺序完全相同的问题。

初始化工作都按预期进行。增量分页也正常工作。问题在于,如果我再次调用 jumpToDay,顺序就会混乱。

如果用户在第-5天,跳到第1天,向左滚动将再次显示第-5天,而不是适当的第0天。这似乎与 UIPageViewController 如何保留对附近页面的引用有关,但我找不到任何强制刷新缓存的方法。

有什么建议吗?


我认为问题与页面控制器保留对附近页面的引用方式无关 - 我甚至不认为它会这样做。在您的数据源实现中,跟踪页面是您的责任。因此,daysAgo或dequeuePreviousDayViewControllerWithDaysBack:方法可能存在问题。 - rdelmar
你解决了这个问题吗?我也遇到了同样的问题。 - Seb Kade
1
@rdelmar 通常我会承认这是我的问题,但“dequeue”方法实际上只是通过一个内部引用给定的“向后天数”来实例化相同类型的控制器。它甚至不执行任何复杂的排队和分配操作,这些任务都由UITableView的dequeueing方法处理。 - Kyle
@SebKade 我还没有找到解决方案,但由于这需要在今天完成,我考虑自己编写页面视图控制器类。如果成功了,我会告诉你的。 - Kyle
那么,daysAgo可能有问题,因为它是唯一决定哪个控制器被实例化的东西。至于自己编写,如果您一次只做一个页面,那么在我看来,页面视图控制器对您并没有太大帮助。我做了一个称为环形控制器的控制器,它允许您向前和向后翻页,还可以跳转到任何页面。实现起来并不难,但我的当前实现会提前实例化所有页面控制器,这并不是很内存高效。 - rdelmar
显示剩余3条评论
13个回答

84

《Programming iOS6》这本书中,Matt Neuburg详细记录了这个问题,我发现他的解决方案比目前被接受的答案更好。这个方案很棒,但有一个负面影响是先动画显示图片,然后突然将该页面替换为所需页面。我觉得这是一种奇怪的用户体验,而Matt的解决方案解决了这个问题。

__weak UIPageViewController* pvcw = pvc;
[pvc setViewControllers:@[page]
              direction:UIPageViewControllerNavigationDirectionForward
               animated:YES completion:^(BOOL finished) {
                   UIPageViewController* pvcs = pvcw;
                   if (!pvcs) return;
                   dispatch_async(dispatch_get_main_queue(), ^{
                       [pvcs setViewControllers:@[page]
                                  direction:UIPageViewControllerNavigationDirectionForward
                                   animated:NO completion:nil];
                   });
               }];

谢谢!那是正确的答案!@Johannes,根据你的问题 - 可以参考这个链接:https://dev59.com/1GIj5IYBdhLWcg3wpmoH#20032131 - skywinder
我不明白如何使用这个解决方案,我在哪里需要使用这段代码?我需要用一个按钮跳转到最后一页。 - Alessio Crestani
你可能想把这段代码放在按钮的动作方法中。你需要存储一个对你的 PVC 的引用,所以在代码示例的第一行中,pvcw 应该设置为 self.pvc。 - djibouti33
1
@kraag22,我很想看看现在它是如何不必要的。您分享的链接似乎与此无关。 - Ali Saeed
3
有人能提供这个的 SWIFT 版本吗?谢谢。 - iYoung
显示剩余2条评论

37

所以我遇到了与你相同的问题,需要能够“跳转”到一个页面,但当我手势返回页面时发现“顺序混乱”。据我所知,页面视图控制器肯定会缓存视图控制器,当您“跳转”到一个页面时,您必须指定方向:向前或向后。然后它假设新的视图控制器是上一个视图控制器的“邻居”,因此自动呈现上一个视图控制器当您手势返回。我发现,只有在使用UIPageViewControllerTransitionStyleScroll而不是UIPageViewControllerTransitionStylePageCurl时才会发生这种情况。页面翻页样式显然不会做相同的缓存,因为如果您“跳转”到一个页面,然后手势返回,它将向数据源提供pageViewController:viewController(Before/After)ViewController:消息,使您能够提供正确的相邻视图控制器。

解决方法: 当执行“跳转”到页面时,您可以首先跳转到页面的相邻页面(animated:NO),然后在该跳转的完成块中,跳转到所需的页面。这将更新缓存,以便在手势返回时显示正确的相邻页面。缺点是您需要创建两个视图控制器:您要跳转到的视图控制器和手势返回后应显示的视图控制器。

这里是我为UIPageViewController编写的类别代码:

@implementation UIPageViewController (Additions)

 - (void)setViewControllers:(NSArray *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction invalidateCache:(BOOL)invalidateCache animated:(BOOL)animated completion:(void (^)(BOOL finished))completion {
    NSArray *vcs = viewControllers;
    __weak UIPageViewController *mySelf = self;

    if (invalidateCache && self.transitionStyle == UIPageViewControllerTransitionStyleScroll) {
        UIViewController *neighborViewController = (direction == UIPageViewControllerNavigationDirectionForward
                                                    ? [self.dataSource pageViewController:self viewControllerBeforeViewController:viewControllers[0]]
                                                    : [self.dataSource pageViewController:self viewControllerAfterViewController:viewControllers[0]]);
        [self setViewControllers:@[neighborViewController] direction:direction animated:NO completion:^(BOOL finished) {
            [mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
        }];
    }
    else {
        [mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
    }
}

@end

你可以测试这个功能,创建一个新的“基于页面”的应用程序,并添加一个“转到”按钮,将跳转到特定的日历月份,然后手势返回。请确保将转换样式设置为滚动。


2
顺便说一句,当我在setViewControllers:direction:animated:completion中将animated设置为NO时,它不会弄乱顺序..!我猜这种“缓存”只会在我们动画setViewControllers的情况下发生。 - akshaynhegde
嗨,我发现你的代码很有用,但是我想知道你是否有Swift版本,因为我在使用weakSelf时遇到了困难。 - shoujo_sm
@Spencer Hall,你应该创建一些示例项目,并在Github上托管,这真的可以帮助其他人 :) - Gurumoorthy Arumugam
这在iOS 10中不再需要。您可以直接使用正常的方法。 - user1416564
我将其更改为viewControllers.firstObject和viewControllers.lastObject,而不是viewControllers [0],以处理数组中多个VC。我认为这应该允许删除transitionStyle检查并更加通用。我的用例仅涉及滚动样式,因此无法进行测试。如果有人能够测试,请评论。谢谢。 - Numan Tariq

5
这是我用Swift编写的解决方案,适用于UIPageViewController的子类:
假设您在viewControllerArray中存储了一个视图控制器数组,并且当前页面索引在updateCurrentPageIndex中。
  private func slideToPage(index: Int, completion: (() -> Void)?) {
    let tempIndex = currentPageIndex
    if currentPageIndex < index {
      for var i = tempIndex+1; i <= index; i++ {
        self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: {[weak self] (complete: Bool) -> Void in
          if (complete) {
            self?.updateCurrentPageIndex(i-1)
            completion?()
          }
          })
      }
    }
    else if currentPageIndex > index {
      for var i = tempIndex - 1; i >= index; i-- {
        self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Reverse, animated: true, completion: {[weak self] (complete: Bool) -> Void in
          if complete {
            self?.updateCurrentPageIndex(i+1)
            completion?()
          }
          })
      }
    }
  }

这难道不会强制将当前控制器和目标之间的所有控制器加载到内存中吗? - Pochi

5
我使用这个函数(我总是处于横屏,双页模式下)。
-(void) flipToPage:(NSString * )index {


int x = [index intValue];
LeafletPageContentViewController *theCurrentViewController = [self.pageViewController.viewControllers   objectAtIndex:0];

NSUInteger retreivedIndex = [self indexOfViewController:theCurrentViewController];

LeafletPageContentViewController *firstViewController = [self viewControllerAtIndex:x];
LeafletPageContentViewController *secondViewController = [self viewControllerAtIndex:x+1 ];


NSArray *viewControllers = nil;

viewControllers = [NSArray arrayWithObjects:firstViewController, secondViewController, nil];


if (retreivedIndex < x){

    [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];

} else {

    if (retreivedIndex > x ){

        [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:NULL];
      } 
    }
} 

1
正是我想要的,谢谢 @jcesar :) (y) - iPhoneDev

3

djibouti33的回答的Swift版本:

weak var pvcw = pageViewController
pageViewController!.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: true) { _ in
        if let pvcs = pvcw {
            dispatch_async(dispatch_get_main_queue(), {
                pvcs.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
            })
        }
    }

似乎在过渡后,我的页面索引被偏移了1。我错过了什么? - zevij

3

需要注意的是,在iOS 10中,情况已经不再如此,您不必再使用被接受的答案解决方案。只需像往常一样继续即可。


@Chiquis 我的意思是,如果你正在使用iOS 10,你需要删除这个方法,否则会出现问题。因此,你必须有两种不同的方法,一种是这种方法,另一种是没有这种方法的方法。 - user1416564

2

我可以确认这个问题,仅在使用UIPageViewControllerTransitionStyleScroll时出现,而不是UIPageViewControllerTransitionStylePageCurl

解决方法:创建一个循环并为每个页面调用UIPageViewController setViewControllers,直到达到所需的页面。

这将保持UIPageViewController内部数据源索引同步。


2

这是唯一的解决方案

-(void)buyAction
{
    isFromBuy = YES;
    APPChildViewController *initialViewController = [self viewControllerAtIndex:4];
    viewControllers = [NSArray arrayWithObject:initialViewController];
    [self.pageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}

-(NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController 
{
    if (isFromBuy) {
        isFromBuy = NO;
        return 5;
    }
    return 0;
}

1
这是一份最新的Swift 3+版本,@djibouti33的答案已清理语法。
weak var weakPageVc = pageVc

pageVc.setViewControllers([page], direction: .forward, animated: true) { finished in
    guard let pageVc = weakPageVc else {
        return
    }

    DispatchQueue.main.async {
        pageVc.setViewControllers([page], direction: .forward, animated: false)
    }
}

1

我有不同的方法,如果您的页面设计成初始化后可更新,则应该是可能的:
当选择手动页面时,我会更新旗帜。

- (void)scrollToPage:(NSInteger)page animated:(BOOL)animated
{
    if (page != self.currentPage) {
        [self setViewControllers:@[[self viewControllerForPage:page]]
                       direction:(page > self.currentPage ?
                                  UIPageViewControllerNavigationDirectionForward :
                                  UIPageViewControllerNavigationDirectionReverse)
                        animated:animated
                      completion:nil];
        self.currentPage = page;
        self.forceReloadNextPage = YES; // to override view controller automatic page cache
    }
}

- (ScheduleViewController *)viewControllerForPage:(NSInteger)page
{
    CustomViewController * scheduleViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"CustomViewController"];
    scheduleViewController.view.tag = page; // keep track of pages using view.tag property
    scheduleViewController.data = [self dataForPage:page];

    if (self.currentViewController)
        scheduleViewController.calendarLayoutHourHeight = self.currentViewController.calendarLayoutHourHeight;

    return scheduleViewController;
}

然后强制下一页重新加载正确的数据:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
    CustomViewController * nextViewController = [pendingViewControllers lastObject];

    // When manual scrolling occurs, the next page is loaded from UIPageViewController cache
    //  and must be refreshed
    if (self.forceReloadNextPage) {
        // calculate the direction of the scroll to know if to load next or previous page
        NSUInteger page = self.currentPage + 1;
        if (self.currentPage > nextViewController.view.tag) page = self.currentPage - 1;

        nextViewController.data = [self dataForPage:page];
        self.forceReloadNextPage = NO;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接