Cómo implementar UIPageViewController que utiliza múltiples ViewControllers

He estado trabajando en una aplicación de prueba simple para conocer los pormenores del UIPageViewController. Lo tengo funcionando, pero no estoy convencido de que mi ejecución sea la mejor. Espero que algunos de ustedes puedan señalarme en la dirección correcta.

Para obtener una comprensión básica, utilicé este tutorial como punto de partida. http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/

El tutorial crea una aplicación que usa un viewController para cada una de las páginas presentadas por UIPageViewController . Sin embargo, necesito utilizar UIPageViewController para desplazarme por páginas que tienen diseños completamente diferentes. Por lo tanto, para llevar el tutorial un paso más allá, creé una aplicación de detalles maestros que usa UIPageViewController en la vista de detalles para mostrar tres controladores de vista diferentes. Me limité a solo mostrar imágenes y tags para esta aplicación de prueba, pero la aplicación que estoy desarrollando tiene tres viewControllers que contendrán ya sea una vista de tabla, imageView y textViews, o algunos campos de texto.

Aquí está el guión gráfico de mi aplicación de prueba.

Storyboard

Utilizo DetailViewController como fuente de datos para PageViewController . En viewDidLoad del DVC establezco las tags e imágenes que se utilizarán en los tres controladores de vista de contenido firstViewController , secondViewController y thirdViewController de esta manera.

 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 

A continuación se muestra el método para crear una instancia de 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 

El código en viewDidLoad es un área que siento que estoy haciendo un trabajo innecesario. No creo que necesite crear una instancia de los tres controladores de vista al cargar el DVC, pero no sabía cómo proporcionar una matriz para los métodos de protocolo viewControllerBeforeViewController ( viewControllerBeforeViewController y viewControllerAfterViewController ).

Aquí está el método 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 resumen, parece que estoy recreando innecesariamente los controladores de vista con cada deslizamiento de una página a otra. Es así como funciona pageViewController o he complicado el proceso. ¡Cualquier entrada sería genial!

SOLUCIÓN

Matt sugirió una solución increíblemente simple al usar identificadores . En mi Storyboard, simplemente marqué la casilla que usa mi identificador Storyboard ya implementado como identificador de restauración

RestorationId

Luego, en viewDidLoad en lugar de crear una matriz de viewControllers, simplemente crea una matriz de cadenas que coincidan con los identificadores de restauración.

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

Finalmente, para determinar el índice en los métodos de delegado, hago esto en lugar de lo que estaba haciendo antes:

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

Ahora funciona sin tener que crear una instancia innecesaria de los controladores de vista.

Como última observación, si alguien está utilizando exactamente lo que tengo aquí, puede deshacerse del siguiente fragmento del método viewControllerAtIndex: .

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

En primer lugar, tiene toda la razón en que los controladores de vista que constituyen las “páginas” de UIPageViewController pueden ser completamente diferentes por naturaleza. Nada dice que tienen que ser instancias de la misma clase de controlador de vista.

Ahora veamos el problema real, que es muy sensatez que necesita una forma de proporcionar el controlador de vista siguiente o anterior dado el controlador de vista actual. Ese es, de hecho, el principal problema al usar un controlador de vista de página.

No sería realmente terrible tener una variedad de controladores de visualización. Después de todo, un controlador de vista es un objeto liviano (es la vista el objeto pesado). Sin embargo, también tienes razón en que la forma en que manejas esto parece torpe.

Mi sugerencia es: si vas a mantener las instancias del controlador de vista en un guión gráfico, ¿por qué no simplemente mantener una matriz de sus identificadores ? Ahora tienes una matriz de tres cadenas. ¿Qué tan simple puedes llegar? También necesitará una única variable de instancia que realice un seguimiento de qué identificador corresponde al controlador de vista que tiene su vista utilizada como la página actual (para que pueda determinar cuál es “siguiente” o “anterior”); esto podría ser solo un índice de indexación en la matriz.

No hay absolutamente nada de malo en crear una instancia de un controlador de vista cada vez que el usuario “da vuelta la página”. Eso es lo que se supone que debes hacer cuando se necesita un controlador de visualización. Y puedes hacer esto fácilmente por identificador.

Finalmente, tenga en cuenta que si usa el estilo de desplazamiento del controlador de vista de página, ni siquiera tendrá que hacerlo, porque el controlador de vista de página almacena en caché los controladores de vista y deja de llamar a los métodos de delegado (o, al menos, los llama menos) .

Me encontré con esta pregunta porque estaba buscando una solución para este mismo problema, gracias a Matt por la guía que me brindó ya Ben por la descripción de la solución.

Creé un proyecto de muestra para comprenderlo yo mismo y, como noté algunos comentarios que pedían código de muestra, lo subí a GitHub. Mi solución imita el enfoque sugerido por Matt y la solución declarada por Ben al:

  • Establecer los ID de restauración en el guión gráfico para cada uno de los controladores de vista que forman parte de PageViewController.
  • Usando un NSArray de objetos NSString que contienen los RestorationIDs mencionados anteriormente.
  • Instanciando el controlador de vista apropiado basado en esta configuración.

Además, el problema de implementación que estaba tratando de resolver requería la capacidad de navegar hacia atrás y hacia delante desde los controles de vista secundarios, por lo que este proyecto de ejemplo también admite esa funcionalidad pidiendo al controlador de vista raíz que vaya a la página anterior o siguiente (esto también podría ser aplicado para ir a una página específica).

Código de muestra en GitHub

Nota al margen: tenía la confianza de que, de forma similar a un UITabBarController, podría simplemente conectar todo desde el Storyboard y especificar el orden de los controles de vista, pero lamentablemente no parece que todavía estemos allí (a partir de Xcode 6 / iOS 8.1). Sin embargo, el código requerido para esta solución es bastante mínimo y directo.

Básicamente, logré obtener una forma ligeramente diferente que se basa en la plantilla proporcionada por los métodos XCode 6.4 (Aplicación basada en la página) y los conocimientos de otros autores (incluido este , @Ben de otras respuestas):

 - (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 *restrationId = viewController.restrationIdentifier; return [self.viewControllersArray indexOfObject:restrationId]; } - (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]; } 
 @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]; }