web-dev-qa-db-fra.com

UIPageViewController, comment accéder correctement à une page spécifique sans perturber l'ordre spécifié par la source de données?

J'ai trouvé quelques questions sur la façon de faire un UIPageViewController sauter vers une page spécifique, mais j'ai remarqué un problème supplémentaire avec le saut qu'aucune des réponses ne semble reconnaître.

Sans entrer dans les détails de mon application iOS (qui est similaire à un calendrier paginé), voici ce que je vis. Je déclare un UIPageViewController, définit le contrôleur de vue actuel et implémente une source de données.

// 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];
}

Modifier la note: j'ai ajouté du code simplifié pour la méthode de mise en file d'attente. Même avec cette implémentation blasphématoire, j'ai exactement le même problème avec l'ordre des pages.

L'initialisation fonctionne comme prévu. La pagination incrémentielle fonctionne également très bien. Le problème est que si j'appelle à nouveau jumpToDay, la commande est brouillée.

Si l'utilisateur est au jour -5 et passe au jour 1, un défilement vers la gauche révélera à nouveau le jour -5 au lieu du jour 0 approprié. Cela semble avoir quelque chose à voir avec la façon dont UIPageViewController conserve les références à pages voisines, mais je ne trouve aucune référence à une méthode qui l'obligerait à actualiser son cache.

Des idées?

62
Kyle

Programmation iOS6 , par Matt Neuburg documente ce problème exact, et j'ai en fait trouvé que sa solution se sent un peu mieux que la réponse actuellement acceptée. Cette solution, qui fonctionne très bien, a un effet secondaire négatif d'animation sur l'image avant/après, puis de remplacement changeant de cette page par la page souhaitée. J'avais l'impression que c'était une expérience utilisateur étrange, et la solution de Matt s'en occupe.

__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];
                   });
               }];
84
djibouti33

J'ai donc rencontré le même problème que vous, où j'avais besoin de pouvoir `` sauter '' sur une page, puis j'ai trouvé `` l'ordre foiré '' lorsque j'ai reculé d'une page. Pour autant que j'ai pu le dire, le contrôleur de vue de page met définitivement en cache les contrôleurs de vue et lorsque vous "sautez" sur une page, vous devez spécifier la direction: avant ou arrière. Il suppose ensuite que le nouveau contrôleur de vue est un "voisin" du contrôleur de vue précédent et présente donc automatiquement le contrôleur de vue précédent lorsque vous faites un geste en arrière. J'ai constaté que cela ne se produit que lorsque vous utilisez UIPageViewControllerTransitionStyleScroll et non UIPageViewControllerTransitionStylePageCurl. Le style de curl de page ne fait apparemment pas la même mise en cache car si vous `` sautez '' sur une page puis faites un geste en arrière, il envoie le message pageViewController:viewController(Before/After)ViewController: à la source de données vous permettant de fournir le contrôleur de vue de voisin correct.

Solution: lorsque vous effectuez un "saut" vers la page, vous pouvez d'abord passer à la page voisine de la page (animated:NO) vous sautez à, puis dans le bloc de fin de ce saut, passez à la page souhaitée. Cela mettra à jour le cache de sorte que lorsque vous revenez en arrière, la page de voisin correcte s'affiche. L'inconvénient est que vous devrez créer deux contrôleurs de vue; celui que vous sautez et celui qui devrait être affiché après avoir fait un geste en arrière.

Voici le code d'une catégorie que j'ai écrite pour 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

Ce que vous pouvez faire pour tester cela est de créer une nouvelle "application basée sur la page" et d'ajouter un bouton "goto" qui "sautera" à un certain mois calendaire, puis reviendra. Veillez à définir le style de transition pour faire défiler.

37
Spencer Hall

J'utilise cette fonction (je suis toujours en mode paysage, 2 pages)

-(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];
      } 
    }
} 
5
jcesarmobile

Voici ma solution Swift à utiliser pour les sous-classes de UIPageViewController:

Supposons que vous stockez un tableau de viewControllers dans viewControllerArray et l'index de la page actuelle dans 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?()
          }
          })
      }
    }
  }
5
confile

Version rapide de la réponse de djibouti33:

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)
            })
        }
    }
3
julian

Il est important de noter que ce n'est plus le cas dans iOS 10 et que vous n'avez plus besoin d'utiliser la solution de réponse acceptée. Continuez comme toujours.

3
user1416564

Je peux confirmer ce problème et qu'il ne se produit qu'en utilisant UIPageViewControllerTransitionStyleScroll et non UIPageViewControllerTransitionStylePageCurl.

Solution: Faire une boucle et appeler UIPageViewController setViewControllers pour chaque tour de page, jusqu'à atteindre la page souhaitée.

Cela permet de synchroniser l'index de source de données interne dans UIPageViewController.

2
Peter Boné

Ce n'est qu'une solution

-(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;
}
2
Jagveer Singh

Voici une mise à jour Swift 3+ version de la réponse de @ djibouti avec une syntaxe nettoyée.

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
Tamás Sengel

J'avais une approche différente, devrait être possible si vos pages sont conçues pour être mises à jour après init:
Lorsqu'une page de manuel est sélectionnée, je mets à jour un indicateur

- (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;
}

puis forcez la page suivante à recharger avec les données correctes:

- (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;
    }
}
1
Yariv Nissim

Si vous n'avez pas besoin d'animer la nouvelle page, comme je ne l'ai pas fait, le code suivant a fonctionné pour moi, appelé "Value Changed" dans le storyboard. Au lieu de passer d'un contrôleur de vue à l'autre, je modifie les données associées au contrôleur de vue actuel.

    - (IBAction)pageControlCurrentPageDidChange:(id)sender
{
    self.currentIndex = self.pageControl.currentPage;
    MYViewController *currentPageViewController = (MYViewController *)self.pageViewController.viewControllers.firstObject;
    currentPageViewController.pageData = [self.pageDataSource dataForPage:self.currentIndex];
    [currentPageViewController updateDisplay];
}

currentIndex est là pour que je puisse mettre à jour la page courante de pageControl lorsque je glisse entre les pages.

pageDataSource dataForPage: retourne un tableau d'objets de données qui sont affichés par les pages.

1
Erich Wood
    let orderedViewControllers = [UIViewController(),UIViewController(), UIViewController()]
    let pageViewController = UIPageViewController()
    let pageControl = UIPageControl()

    func jump(to: Int, completion: @escaping (_ vc: UIViewController?) -> Void){

        guard orderedViewControllers.count > to else{
            //index of bounds
            return
        }

        let toVC = orderedViewControllers[to]

        var direction: UIPageViewController.NavigationDirection = .forward

        if pageControl.currentPage < to {
            direction = .forward;
        } else {
            direction = .reverse;
        }

        pageViewController.setViewControllers([toVC], direction: direction, animated: true) { _ in
            DispatchQueue.main.async {
                self.pageViewController.setViewControllers([toVC], direction: direction, animated: false){ _ in
                    self.pageControl.currentPage = to
                        completion(toVC)

                }
            }
        }
    }

TILISATION:

self.jump(to: 5) { (vc) in
    // you can do anything for new vc.
}
0
Okan

Je me débattais avec ce problème depuis longtemps. Pour moi, j'avais une charge UIPageViewController (je l'ai appelé PageController) à partir du storyboard et j'y ajoute un UIViewController 'ContentVC'.

Je laisse le ContentVC s'occuper des données à charger dans la zone de contenu et je laisse PageController s'occuper des mises à jour glissantes/goto/PageIndicator. ContentVC a un ivar CurrentPageIndex et envoie cette valeur à PageController pour que PageController sache sur quelle page il se trouve. Dans mon fichier .m qui a PageController j'ai ces deux méthodes.

Notez que j'ai utilisé défini sur 0 et donc chaque fois que PageVC se recharge, il passe à la première page dont je ne veux pas, [self viewControllerAtIndex: 0].

- (void)setPageForward
{  
  ContentVC *FirstVC = [self viewControllerAtIndex:[CurrentPageIndex integerValue]];

  NSArray *viewControllers = @[FirstVC];
  [PageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}

Cette deuxième méthode est la méthode DataSource de PageViewController. presentationIndexForPageViewController définira le point en surbrillance sur la bonne page (la page que vous voulez). Notez que si nous retournons 0 ici, l'indicateur de page mettra en évidence le premier point qui indique la première page et nous ne voulons pas cela.

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController 
{
  return [CurrentPageIndex integerValue];
}
0
Ohmy