Eliminar un controlador de vista de UIPageViewController

Es extraño que no haya una manera directa de hacer esto. Considere la siguiente situación:

  1. Usted tiene un controlador de vista de página con 1 página.
  2. Agregue otra página (total 2) y desplácese hasta ella.
  3. Lo que quiero es que, cuando el usuario se desplaza de vuelta a la primera página, la segunda página se elimine y desasigne, y el usuario ya no pueda volver a esa página.

Intenté eliminar el controlador de vista como un controlador de vista secundaria después de que se complete la transición, pero aún así me permite desplazarme de vuelta a la página vacía (no “cambia el tamaño” de la vista de página)

¿Es lo que quiero hacer posible?

Si bien las respuestas aquí son todas informativas, hay una forma alternativa de manejar el problema, que se da aquí:

UIPageViewController navega a la página incorrecta con el estilo de transición de desplazamiento

La primera vez que busqué una respuesta a este problema, la forma en que estaba redactando mi búsqueda me dejó atónita a esta pregunta, y no a la que acabo de vincular, por lo que me sentí obligado a publicar una respuesta que vincule a esta otra pregunta, ahora que lo he encontrado, y también estoy elaborando un poco.

El problema está descrito muy bien por Matt aquí :

Esto es realmente un error en UIPageViewController. Ocurre solo con el estilo de desplazamiento (UIPageViewControllerTransitionStyleScroll) y solo después de llamar a setViewControllers: direction: animated: completion: with animated: YES. Por lo tanto, hay dos soluciones:

No use UIPageViewControllerTransitionStyleScroll.

O bien, si llama a setViewControllers: direction: animated: completion :, use solo animated: NO.

Para ver claramente el error, llame a setViewControllers: direction: animated: completion: y luego, en la interfaz (como usuario), navegue a la izquierda (atrás) manualmente a la página anterior. Volverá a la página incorrecta: no a la página anterior, sino a la página en la que estaba cuando estableció ViewControllers: direction: animated: completion: se invocó.

El motivo del error parece ser que, al usar el estilo de desplazamiento, UIPageViewController realiza algún tipo de almacenamiento en caché interno. Por lo tanto, después de la llamada a setViewControllers: direction: animated: completion :, no puede borrar su caché interna. Cree que sabe lo que es la página anterior. Por lo tanto, cuando el usuario navega hacia la izquierda a la página anterior, UIPageViewController no puede llamar al método dataSource pageViewController: viewControllerBeforeViewController :, o lo llama con el controlador de vista actual incorrecto.

Esta es una buena descripción, no es exactamente el problema que se menciona en esta pregunta, pero está muy cerca. Observe la línea sobre si establece setViewControllers con animated:NO forzará a UIPageViewController a volver a consultar su fuente de datos la próxima vez que el usuario realice un gesto, ya que ya no sabe dónde está o qué controles de vista están próximos a su controlador de vista actual.

Sin embargo, esto no funcionó para mí porque hubo momentos en los que tuve que mover de forma programática el PageView con una animación.

Entonces, mi primer pensamiento fue llamar a setViewControllers con una animación, y luego en el bloque de finalización llamar al método nuevamente con cualquier vista que el controlador mostrara ahora, pero sin animación. Entonces, el usuario puede panoramizar, está bien, pero luego volvemos a llamar al método para que la vista de página se restablezca.

Lamentablemente, cuando lo intenté, comencé a recibir extraños “errores de aserción” del controlador de vista de página. Se ven algo como esto:

*** Fallo de aserción en – [UIPageViewController queuingScrollView: …

Al no saber exactamente por qué sucedía esto, di marcha atrás y eventualmente comencé a usar la respuesta de Jai como una solución, creando un UIPageViewController completamente nuevo, empujándolo a un UINavigationController, y luego sacando el viejo. Bruto, pero funciona, principalmente. He estado descubriendo que todavía estoy recibiendo fallas de aserción ocasionales del UIPageViewController, como este:

*** Error de aserción en – [UIPageViewController queuingScrollView: didEndManualScroll: toRevealView: dirección: animated: didFinish: didComplete:], /SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $ 1 = 154507824 Controlador de vista que no gestiona la vista visible>

Y la aplicación falla. ¿Por qué? Bueno, buscando, encontré esta otra pregunta que mencioné arriba, y particularmente la respuesta aceptada que defiende mi idea original, de simplemente llamar a setViewControllers: animated:YES y luego tan pronto como termine llamando a setViewControllers: animated:NO con el mismo ver los controladores para reiniciar el UIPageViewController, pero tenía el elemento faltante: volver a llamar ese código en la cola principal! Aquí está el código:

 __weak YourSelfClass *blocksafeSelf = self; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){ if(finished) { dispatch_async(dispatch_get_main_queue(), ^{ [blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller }); } }]; 

¡Guauu! La única razón por la que esto realmente tiene sentido para mí es porque he visto la WWDC 2012 Session 211, Construyendo Interfaces de Usuario Simultáneas en iOS (disponible aquí con una cuenta de desarrollador). Ahora recuerdo que intentar modificar los objetos de origen de datos que los objetos UIKit (como UIPageViewController) dependen de, y hacerlo en una cola secundaria, puede causar algunos lockings desagradables.

Lo que nunca he visto especialmente documentado, pero ahora debo asumir que es el caso y seguir leyendo, es que el bloque de finalización para una animación se realiza en una cola secundaria, no en la principal. Así que la razón por la cual UIPageViewController estaba graznando y dando fallas de aserción, cuando originalmente intenté llamar a setViewControllers animated:NO en el bloque de finalización de setViewControllers animated:YES y también ahora que simplemente estoy usando un UINavigationController para presionar un nuevo UIPageViewController (pero haciéndolo, de nuevo, en el bloque de finalización de setViewControllers animated:YES ) es porque todo está sucediendo en esa cola secundaria.

Es por eso que ese pedazo de código funciona perfectamente, porque vienes del bloque de finalización de animación y lo envías a la cola principal para que no cruces las transmisiones con UIKit. Brillante.

De todos modos, quería compartir este viaje, en caso de que alguien se encuentre con este problema.

EDITAR: versión Swift aquí , si alguien está interesado.

Al concluir la gran respuesta de Matt Mc, se podría agregar el siguiente método a una subclase de UIPageViewController, que de esa manera permite el uso de setViewControllers: direction: animated: completion: como estaba destinado a ser usado si el error no estuviera presente.

 - (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion { if (!animated) { [super setViewControllers:viewControllers direction:direction animated:NO completion:completion]; return; } [super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){ if (finished) { dispatch_async(dispatch_get_main_queue(), ^{ [super setViewControllers:viewControllers direction:direction animated:NO completion:completion]; }); } else { if (completion != NULL) { completion(finished); } } }]; } 

Ahora, simplemente llame a setViewControllers: direction: animated: completion: en la clase / subclases que implementa este método, y debería funcionar como se esperaba.

maq tiene razón Si está utilizando la transición de desplazamiento, la eliminación de un controlador de vista secundario desde el UIPageViewController no impide que la “página” eliminada vuelva a aparecer en la pantalla si el usuario navega hacia ella. Si está interesado, así es como eliminé el controlador de vista infantil desde UIPageViewController.

 // deleteVC is a child view controller of the UIPageViewController [deleteVC willMoveToParentViewController:nil]; [deleteVC.view removeFromSuperview]; [deleteVC removeFromParentViewController]; 

Controlador de vista deleteVC se elimina de la propiedad childViewControllers del UIPageViewController, pero aún aparece en pantalla si el usuario navega hacia él.

Hasta que alguien más inteligente que yo encuentre una solución elegante, aquí hay una solución (es un truco, así que debes preguntarte si realmente necesitas eliminar páginas de un UIPageViewController).

Estas instrucciones suponen que solo se muestra una página a la vez.

Después de que el usuario toque un botón que indica que desea eliminar la página, vaya a la página siguiente o anterior usando el método setViewControllers: direction: animated: completion :. Por supuesto, debe eliminar el contenido de la página de su modelo de datos.

A continuación (y aquí está el truco), cree y configure un nuevo UIPageViewController y cárguelo en primer plano (es decir, frente al otro UIPageViewController). Asegúrese de que el nuevo UIPageViewController comience mostrando la misma página que se mostró anteriormente. Su nuevo UIPageViewController obtendrá nuevos controladores de vista del origen de datos.

Finalmente, descargue y destruya el UIPageViewController que está en segundo plano.

De todos modos, maq hizo una muy buena pregunta. Desafortunadamente, no tengo suficientes puntos de reputación para votar la pregunta. Ah, atrévete a soñar … Algún día tendré 15 puntos de reputación.

Pondré esta respuesta aquí solo para mi futura referencia y si ayuda a alguien, lo que terminé haciendo fue:

  1. Eliminar la página y avanzar a la siguiente página
  2. En el bloque de finalización de setViewControllers , creé / inicié un nuevo UIPageViewController con los datos modificados (elemento eliminado), y lo empujé sin animar, por lo que nada cambia en la pantalla (mi UIPageViewController está contenido dentro de un UINavigationController)
  3. Después de presionar el nuevo UIPageViewController, obtenga una copia de la matriz viewControllers del UINavigationController, elimine el penúltimo controlador de vista (que es el antiguo UIPageViewController)
  4. No hay paso 4 – ¡listo!

Para mejorar esto Debería detectar si pageView se está desplazando o no antes de setViewControllers.

 var isScrolling = false func viewDidLoad() { ... for v in view.subviews{ if v.isKindOfClass(UIScrollView) { (v as! UIScrollView).delegate = self } } } func scrollViewWillBeginDragging(scrollView: UIScrollView){ isScrolling = true } func scrollViewDidEndDecelerating(scrollView: UIScrollView){ isScrolling = false } func jumpToVC{ if isScrolling { //you should not jump out when scrolling return } setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in if succ { dispatch_async(dispatch_get_main_queue(), { () -> Void in self.setViewControllers([vc], direction:direction, animated:false, completion:nil) }) } }) } 

Este problema existe cuando intentas cambiar viewControllers durante la animación de gestos de deslizamiento entre viewControllers. Para resolver este problema, hice una bandera simple para descubrir cuando el controlador de vista de página está durante la transición.

 - (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers { self.pageViewControllerTransitionInProgress = YES; } - (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed { self.pageViewControllerTransitionInProgress = NO; } 

Y cuando estoy tratando de ver el controlador de vista estoy comprobando si hay una transición en progreso.

 - (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated { if (self.pageViewControllerTransitionInProgress) { return; } [self.pageContentViewController setViewControllers:@[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) { } } 

Solo estoy aprendiendo esto yo mismo, así que tómelo con un grano de sal, pero por lo que entiendo, necesita cambiar la fuente de datos del controlador pageview, no eliminar el controlador de vista. La cantidad de páginas que se muestran en un controlador de vista de página está determinada por su origen de datos, no por los controladores de vista.

En realidad creo que resolví el problema. Esto es lo que estaba sucediendo en mi aplicación:

  1. La instancia de subclase UIViewController se eliminó de UIPageViewController quitándola del modelo y estableciendo la propiedad viewControllers de UIPageViewController en diferentes ViewController. Esto es suficiente para hacer el trabajo. No es necesario hacer ningún código de contención del controlador en ese controlador
  2. El ViewController se había ido, pero aún podía desplazarme deslizándolo directamente en mi caso.
  3. Mi problema fue esto. Estaba agregando un reconocedor de gestos personalizado a ViewController que muestra UIPageViewController. Este reconocedor se mantuvo como una propiedad fuerte en el controlador que también poseía UIPageViewController.
  4. REVISIÓN: antes de perder el acceso al ViewController que se descartaba, me aseguré de limpiar adecuadamente toda la memoria que usa (dealloc) y eliminar el reconocedor de gestos
  5. Su kilometraje puede variar, pero no veo el punto de que esta solución sea incorrecta, cuando algo no funciona, sospecho primero de mi código 🙂

Tuve una situación similar en la que quería que el usuario pudiera “tocar y eliminar” cualquier página del UIPageViewController. Después de jugar un poco con eso, encontré una solución más simple que las descritas anteriormente:

  1. Capture el índice de página de la “página que está muriendo”.
  2. Verifique si la “página que está muriendo” es la última página.
    • Esto es importante porque si es la última página, necesitamos desplazarnos hacia la izquierda (si tenemos las páginas “ABC” y eliminar C, nos desplazaremos a B).
    • Si no es la última página, nos desplazaremos hacia la derecha (si tenemos las páginas “ABC” y eliminamos B, nos desplazaremos a C).
  3. Haga un salto temporal a un lugar “seguro” (idealmente el último). Use animated: NO para que esto suceda instantáneamente.

     UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; 
  4. Elimine la página seleccionada del modelo / fuente de datos.

  5. Ahora, si es la última página:

    • Ajuste su modelo para seleccionar la página de la izquierda.
    • Obtenga el controlador de visualización de la izquierda, tenga en cuenta que ESTE ES EL MISMO que el que recuperó en el paso 3.
    • Es importante hacer esto DESPUÉS de haber eliminado la página de la fuente de datos porque la actualizará.
    • Salta a ella, esta vez con animada: SÍ.

       jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil]; 
  6. En el caso donde NO era la última página:

    • Ajuste su modelo para seleccionar la página de la derecha.
    • Obtenga el controlador de visualización a la derecha, tenga en cuenta que este no es el que recuperó en el paso 3. En mi caso, verá que es el que está en dyingPageIndex, porque la página agonizante ya se ha eliminado del modelo.
    • Nuevamente, es importante hacer esto DESPUÉS de haber eliminado la página de la fuente de datos porque la actualizará.
    • Salta a ella, esta vez con animada: SÍ.

       jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil]; 
  7. ¡Eso es! Esto funciona bien en XCode 6.1.1.

Código completo a continuación. Este código está en mi UIPageViewController y se llama a través de un delegado de la página que se eliminará. En mi caso, no se permitió eliminar la primera página, ya que contiene cosas diferentes del rest de las páginas. Por supuesto, debes sustituir:

  • YourUIViewController: con la clase de sus páginas individuales.
  • YourTotalNumberOfPagesFromModel: con el número total de páginas en su modelo
  • [YourModel deletePage:] con el código para eliminar la página agonizante de su modelo

     - (void)deleteAViewController:(id)sender { YourUIViewController *dyingGroup = (YourUIViewController *)sender; NSUInteger dyingPageIndex = dyingGroup.pageIndex; // Check to see if we are in the last page as it's a special case. BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count); // Make a temporary jump back - make sure to use animated:NO to have it jump instantly UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; // Now delete the selected group from the model, setting the target [YourModel deletePage:dyingPageIndex]; if (isTheLastPage) { // Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source. // This time we're using animated:YES jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil]; } else { // Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex]; [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil]; } } 
 NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray]; [mArray removeObject:userToBlock]; self.userArray = mArray; UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1]; NSArray *viewControllers = @[startingViewController]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil]; 

No tuve suficiente tiempo para leer los comentarios anteriores, pero esto funcionó para mí. Básicamente elimino los datos (en mi caso un usuario) y luego me desplazo a la página anterior. Funciona de maravilla. Espero que esto ayude a aquellos que buscan una solución rápida.

Lo hizo lo siguiente en Swift 3.0

 fileprivate var isAnimated:Bool = false override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) { if self.isAnimated { delay(0.5, closure: { self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion) }) }else { super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion) } } extension SliderViewController:UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { self.isAnimated = true } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { self.isAnimated = finished } } 

Y aquí la función de retardo

 func delay(_ delay:Double, closure:@escaping ()->()) { DispatchQueue.main.asyncAfter( deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) }