web-dev-qa-db-fra.com

Comment implémenter UIPageViewController utilisant plusieurs ViewControllers

J'ai travaillé sur une application de test simple pour apprendre les tenants et les aboutissants de UIPageViewController. Je le fais fonctionner mais je ne suis pas convaincu que mon exécution est la meilleure façon. J'espère que certains d'entre vous pourront me diriger dans la bonne direction.

Pour acquérir des connaissances de base, j’ai utilisé ce didacticiel comme point de départ . http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/

Le tutoriel crée une application qui utilise une viewController pour chacune des pages présentées par la UIPageViewController. Cependant, je dois utiliser UIPageViewController pour faire défiler des pages de présentations complètement différentes. Par conséquent, pour aller plus loin dans le didacticiel, j'ai créé une application maître-détail qui utilise UIPageViewController dans la vue détaillée pour afficher trois contrôleurs de vue différents. Je me suis contenté d'afficher des images et des étiquettes pour cette application de test, mais l'application que je suis en train de créer comporte trois viewControllers qui contiendront soit une vue de table, une imageView et une vue de texte, ou encore un champ de texte.

Voici le storyboard de mon application de test.

Storyboard

J'utilise DetailViewController comme source de données pour PageViewController. Dans viewDidLoad du DVC, j'établis les étiquettes et les images qui seront utilisées dans les trois contrôleurs de vue de contenu firstViewController, secondViewController et thirdViewController de cette manière.

if ([[self.detailItem description] isEqualToString:@"F14's"]) {
    //Here the page titles and images arrays are created 
    _pageTitles = @[@"Grim Reapers", @"Breakin the Barrier!", @"Top Gun"];
    _pageImages = @[@"F14_Grim.jpg", @"F14boom.jpg", @"F14_topgun.jpg"];

    //Here I call a method to instantiate the viewControllers 
    FirstController *selectedController = [self viewControllerAtIndex:0];
    SecondController *nextController = [self viewControllerAtIndex:1];
    ThirdController *lastController = [self viewControllerAtIndex:2];
    [_vc addObject:selectedController];
    [_vc addObject:nextController];
    [_vc addObject:lastController];
    _vc1 = @[selectedController];


} else if ([[self.detailItem description] isEqualToString:@"F35's"]){
    //code is above is repeated

Voici la méthode pour instancier les viewControllers

- (UIViewController *)viewControllerAtIndex:(NSUInteger)index
{
    if (([self.pageTitles count] == 0) || (index >= [self.pageTitles count])) {
        return nil;
    }

    // Create a new view controller and pass suitable data.
    if (index == 0) {
        FirstController *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"FirstPageController"];
        fvc.imageFile = self.pageImages[index];
        fvc.titleText = self.pageTitles[index];
        fvc.pageIndex = index;
        if ([_vc count]) {
             //Here I have to replace the viewController each time it is recreated
             [_vc replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    } else if (index == 1) {
//Code is repeated for remaining viewControllers

Le code dans viewDidLoad est un domaine dans lequel je sens un travail inutile. Je ne pense pas avoir besoin d'instancier les trois contrôleurs de vue lors du chargement du DVC, mais je ne savais pas comment fournir un tableau pour les méthodes de protocole UIPageViewControllerDataSource (viewControllerBeforeViewController et viewControllerAfterViewController).

Voici la méthode viewControllerBefore...

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{

    NSUInteger index = [_vc indexOfObject:viewController];

    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;

    //notice here I call my instantiation method again essentially duplicating work I have already done!
    return [self viewControllerAtIndex:index];
}

En résumé, il semble que je recrée inutilement les contrôleurs de vue à chaque balayage d'une page à une autre. Est-ce juste la façon dont fonctionne le pageViewController ou ai-je bien compliqué le processus. Toute entrée serait géniale!

SOLUTION

Matt a suggéré une solution incroyablement simple en utilisant identifiants. Dans mon Storyboard, j'ai simplement coché la case qui utilise mon identifiant Storyboard déjà implémenté comme identifiant de restauration

RestorationId

Ensuite, dans viewDidLoad plutôt que de créer un tableau de viewControllers, créez simplement un tableau de chaînes qui correspondent aux identificateurs de restauration.

if ([[self.detailItem description] isEqualToString:@"F14's"]) {
    _pageTitles = @[@"Grim Reapers", @"Breakin the Barrier!", @"Top Gun"];
    _pageImages = @[@"F14_Grim.jpg", @"F14boom.jpg", @"F14_topgun.jpg"];
    FirstController *selectedController = [self viewControllerAtIndex:0];
    [_vc addObject:@"FirstPageController"];
    [_vc addObject:@"SecondPageController"];
    [_vc addObject:@"ThirdPageController"];
    _vc1 = @[selectedController];

Enfin, pour déterminer l’index dans les méthodes de délégation, procédez comme suit plutôt que ce que je faisais auparavant:

NSString * ident = viewController.restorationIdentifier;
NSUInteger index = [_vc indexOfObject:ident];

Il fonctionne maintenant sans avoir à instancier inutilement les contrôleurs de vue.

Pour finir, si quelqu'un utilise exactement ce que j'ai ici, vous pouvez vous débarrasser de l'extrait suivant de la méthode viewControllerAtIndex:.

if ([_vc count]) {
     //Here I have to replace the viewController each time it is recreated
     [_vc replaceObjectAtIndex:0 withObject:fvc];
}
34
Ben

Tout d’abord, vous avez absolument raison de dire que les contrôleurs de vue qui constituent les "pages" de UIPageViewController peuvent être de nature complètement différente. Rien ne dit qu'ils doivent être des instances de la classe de contrôleur de vue identique.

Passons maintenant au problème actuel, à savoir que vous avez vraiment besoin d'un moyen de fournir le contrôleur de vue suivant ou précédent en fonction du contrôleur de vue actuel. C’est en effet le principal problème lors de l’utilisation d’un contrôleur de vue de page.

Il ne serait pas vraiment terrible de disposer d’un grand nombre de contrôleurs de vue. Après tout, un contrôleur de vue est un objet léger (c’est la vue qui est l’objet poids lourd). Cependant, vous avez également raison de dire que la façon dont vous gérez cela semble maladroite.

Ma suggestion est la suivante: si vous allez conserver les occurrences du contrôleur de vue dans un storyboard, pourquoi ne pas simplement conserver un tableau de leurs identificateurs? Maintenant, vous avez un tableau de trois chaînes. Comment pouvez-vous être simple? Vous aurez également besoin d’une seule variable d’instance permettant de savoir quel identificateur correspond au contrôleur de vue. Cette vue est utilisée comme page actuelle (afin que vous puissiez déterminer laquelle est "suivante" ou "précédente". "); cela pourrait simplement être un index entier dans le tableau.

Il n'y a alors absolument rien de mal à instancier un contrôleur de vue à chaque fois que l'utilisateur "tourne la page". C'est ce que vous êtes supposé _ faire lorsqu'un contrôleur de vue est nécessaire. Et vous pouvez facilement le faire par identifiant.

Enfin, notez que si vous utilisez le style de défilement du contrôleur de vue de page, vous n’aurez même pas à le faire car le contrôleur de vue de page met en cache les contrôleurs de vue et arrête d’appeler les méthodes de délégation (ou, au moins, de les appeler moins). .

40
matt

Je suis tombé sur cette question alors que je cherchais une solution à ce même problème - merci à Matt pour les conseils qu'il a fournis et à Ben pour la description de la solution.

J'ai construit un exemple de projet pour comprendre cela moi-même et depuis que j'ai remarqué quelques commentaires demandant un exemple de code, j'ai téléchargé ceci dans GitHub. Ma solution imite l'approche suggérée par Matt et la solution indiquée par Ben en:

  • Définition des ID de restauration dans le storyboard pour chacun des contrôleurs de vue faisant partie de PageViewController.
  • Utilisation d'un objet NSArray d'objets NSString contenant les identificateurs de restauration susmentionnés.
  • Instanciation du contrôleur de vue approprié en fonction de cette configuration.

De plus, le problème de mise en œuvre que je tentais de résoudre nécessitait la possibilité de naviguer en arrière/en avant à partir des contrôleurs de vue enfant. Cet exemple de projet prend également en charge cette fonctionnalité en demandant au contrôleur de vue racine de passer à la page précédente ou suivante (cela pourrait également être appliqué pour aller à une page spécifique).

Exemple de code sur GitHub

Note latérale: J'espérais bien que, comme un contrôleur UITabBar, je pourrais simplement tout câbler depuis le Storyboard et spécifier l'ordre des contrôleurs de vue, mais hélas, il ne semble pas que nous y soyons encore (à partir de Xcode 6/iOS 8.1). Le code requis pour cette solution est toutefois assez simple et simple.

8
Derek Lee

En gros, j'ai réussi à obtenir une manière légèrement différente qui est basée sur le modèle fourni par les méthodes XCode 6.4 (Application basée sur la page) et les idées d'autres auteurs (y compris this , @Ben à partir d'autres réponses):

- (void)viewDidLoad {
    [super viewDidLoad];

    self.viewControllersArray = @[@"FirstViewController", @"SecondViewController"];
...
}

...

- (UIViewController *)viewControllerAtIndex:(NSUInteger)index {

    UIViewController *childViewController = [self.storyboard instantiateViewControllerWithIdentifier:[self.viewControllersArray objectAtIndex:index]];
    //childViewController.index = index;
    return childViewController;

}

- (NSUInteger)indexOfViewController:(UIViewController *)viewController {
    NSString *restorationId = viewController.restorationIdentifier;
    return [self.viewControllersArray indexOfObject:restorationId];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController:(UIViewController *)viewController];
    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;
    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController:(UIViewController *)viewController];
    if (index == NSNotFound) {
        return nil;
    }

    index++;
    if (index == [self.viewControllersArray count]) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}
1
Darius Miliauskas
@interface ViewController ()
{
    NSMutableArray * StoryboardIds;
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    StoryboardIds = [[NSMutableArray alloc]init];
    [StoryboardIds addObject:@"vc1"];
    [StoryboardIds addObject:@"vc2"];

    UIViewController *selectedController = [self viewControllerAtIndex:0];

    self.ProfilePageViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"PageControl"];
    self.ProfilePageViewController.dataSource = self;

    _viewcontrollers = [NSMutableArray new];

    [_viewcontrollers addObject:selectedController];

    [self.ProfilePageViewController setViewControllers:_viewcontrollers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];

    // Change the size of page view controller
    self.ProfilePageViewController.view.frame = CGRectMake(0, 0, self.bodypage.frame.size.width, self.bodypage.frame.size.height);

    [self addChildViewController:self.ProfilePageViewController];
    [self.ProfilePageViewController.view setFrame:self.bodypage.bounds];
    [self.bodypage addSubview:self.ProfilePageViewController.view];
    [self.ProfilePageViewController didMoveToParentViewController:self];

}
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index
{
    if (([StoryboardIds count] == 0) || (index >= [StoryboardIds count])) {
        return nil;
    }

    if (index == 0) {
        vc1 *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"vc1"];
        fvc.Pageindex = index;
        if ([_viewcontrollers count]) {
            [_viewcontrollers replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    }
    else
    {
        vc2 *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"vc2"];
        fvc.Pageindex = index;
        if ([_viewcontrollers count]) {
            [_viewcontrollers replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    }

}

-(NSUInteger)indexofViewController
{
    UIViewController *currentView = [self.ProfilePageViewController.viewControllers objectAtIndex:0];

    if ([currentView isKindOfClass:[vc2 class]]) {
        return 1;
    }
    else{
        return 0;
    }

}


- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexofViewController];

    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;
    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexofViewController];

    if (index == NSNotFound) {
        return nil;
    }

    index++;

    if (index == [StoryboardIds count]) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}
0
Raja Bhuma