example bar ios cocoa-touch uinavigationcontroller uinavigationitem

bar - ¿Mejores prácticas para manejar los cambios en el UINavigationItem de controladores de vista secundarios en un controlador de contenedor?



navigation controller swift 4 example (4)

Supongamos que tengo un controlador de contenedor que acepta una matriz de UIViewControllers y los coloca para que el usuario pueda desplazarse hacia la izquierda y la derecha para hacer la transición entre ellos. Este controlador de contenedor está envuelto dentro de un controlador de navegación y se convierte en el controlador de vista raíz de la ventana principal de la aplicación.

Cada controlador secundario realiza una solicitud a la API y carga una lista de elementos que se muestran en una vista de tabla. En función de los elementos que se muestran, se puede agregar un botón a la barra de navegación que permite al usuario actuar sobre todos los elementos en la vista de tabla.

Debido a que UINavigationController solo usa los UINavigationItems de sus controladores de vista secundarios, el controlador de contenedor necesita actualizar su UINavigationItem para estar en sincronía con el UINavigationItem de sus hijos.

Parece que hay dos escenarios que el controlador de contenedor necesita manejar:

  1. El controlador de vista seleccionado del controlador de contenedor cambia y, por lo tanto, el UINavigationItem del controlador de contenedor debe actualizarse para imitar el UINavigationItem del controlador de vista seleccionado.
  2. Un controlador secundario actualiza su UINavigationItem y el controlador de contenedor debe conocer el cambio y actualizar su UINavigationItem para que coincida.

Las mejores soluciones que he encontrado son:

  1. En el setSelectedViewController: método, consulte el elemento de navegación del controlador de vista seleccionado y actualice las propiedades de título de barra izquierda, el de barra de botón y los elementos de título de UINavigationItem del controlador de contenedor para que sean iguales a los de UINavigationItem del controlador de vista seleccionado.
  2. En el método setSelectedViewController KVO en leftBarButtonItems, rightBarButtonItems y propiedad de título de UINavigationItem del controlador de vista seleccionado y cada vez que una de esas propiedades cambie el UINavigationItem del controlador de contenedor.

Este es un problema recurrente con muchos de los controladores de contenedor que he escrito y parece que no puedo encontrar ninguna solución documentada para estos problemas.

¿Cuáles son algunas de las soluciones que la gente ha encontrado para este problema?


¿Ha considerado NO envolver su controlador de vista de contenedor en un UINavigationController y simplemente agregar una barra de UINavigation a su vista? Luego puede empujar los elementos de navegación del controlador de vista hijo directamente a esa barra de navegación. Esencialmente, su controlador de vista de contenedor reemplazaría un UIViewController normal.


La respuesta aceptada funciona, pero rompe el contrato en UIViewController, sus controladores secundarios ahora están estrechamente relacionados con su categoría personalizada y deben usar sus métodos alternativos para funcionar correctamente ... Tuve este problema al usar el contenedor RBStoryboardLink, y también en un controlador de mi barra de pestañas personalizado, por lo que era importante que fuera encapsulado fuera de una clase de contenedor dada, así que creé una clase que tiene una propiedad mirrorVC (generalmente establecida en el contenedor, la que escuchará las notificaciones) y algunos métodos de registro / anulación de registro (para elementos de navegación, artículos de barra de herramientas, artículos de barra, según sus necesidades lo consideren adecuado). Por ejemplo, al registrar / desregistrar para toolbarItems:

static void *myContext = &myContext; -(void)registerForToolbarItems:(UIViewController*)viewController { [viewController addObserver:self forKeyPath:@"toolbarItems" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:myContext]; } -(void)unregisterForToolbarItems:(UIViewController*)viewController { [viewController removeObserver:self forKeyPath:@"toolbarItems" context:myContext]; }

La acción de observación se encargará de recibir los nuevos valores y enviarlos a mirrorVC:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if(context == myContext) { id newKey = [change objectForKey:NSKeyValueChangeNewKey]; id oldKey = [change objectForKey:NSKeyValueChangeOldKey]; //no need to mirror if the value is the same if ([newKey isEqual:oldKey]) return; //nil values comes packaged in NSNull if (newKey == [NSNull null]) newKey = nil; //handle each of the possibly registered mirrored properties... if ([keyPath isEqualToString:@"navigationItem.leftBarButtonItem"]) { self.mirrorVC.navigationItem.leftBarButtonItem = newKey; } //... //as many more properties as you need forwarded... else if ([keyPath isEqualToString:@"toolbarItems"]) { [self.mirrorVC setToolbarItems:newKey animated:YES]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }

Luego, en su contenedor, en los momentos correctos, se registra y anula el registro.

[_selectedViewController unregister...] _selectedViewController = selectedViewController; [_selectedViewController register...]

Sin embargo, debe estar al tanto de una posible trampa: no todas las propiedades deseables son compatibles con KVO, y las que sí lo hacen no están documentadas, por lo que pueden dejar de ser o comportarse mal en cualquier momento. La propiedad toolbarItems, por ejemplo, no lo es. Creé una categoría UIViewController basada en esta esencia ( https://gist.github.com/brentdax/5938102 ) que habilita las notificaciones de KVO para que funcione en este escenario. Nota: lo esencial anterior no fue necesario para UINavigationItem, iOS 5 ~ 7 envía las notificaciones KVO adecuadas para ello, con esa categoría recibiría notificaciones dobles para UINavigationItems. Funcionó a la perfección para toolbarItems!


Por lo tanto, la solución que implementé actualmente es crear una categoría en UIViewController con métodos que le permitan configurar los botones de la barra derecha del elemento de navegación de ese controlador y luego ese controlador publique una notificación para que cualquiera que se preocupe sepa que los elementos del botón de la barra derecha tienen ha cambiado

En mi controlador de contenedor escucho esta notificación del controlador de vista actualmente seleccionado y actualizo el elemento de navegación del controlador de contenedor en consecuencia.

En mi caso, el controlador del contenedor anula el método en la categoría para que pueda mantener una copia local de los elementos del botón de la barra derecha que se le han asignado y, si se produce alguna notificación, concatena los elementos del botón de la barra derecha con el de su hijo y luego envía una notificación en caso de que también esté dentro de un controlador de contenedor.

Aquí está el código que estoy usando.

UIViewController + ContainerNavigationItem.h

#import <UIKit/UIKit.h> extern NSString *const UIViewControllerRightBarButtonItemsChangedNotification; @interface UIViewController (ContainerNavigationItem) - (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems; - (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem; @end

UIViewController + ContainerNavigationItem.m

#import "UIViewController+ContainerNavigationItem.h" NSString *const UIViewControllerRightBarButtonItemsChangedNotification = @"UIViewControllerRightBarButtonItemsChangedNotification"; @implementation UIViewController (ContainerNavigationItem) - (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems { [[self navigationItem] setRightBarButtonItems:rightBarButtonItems]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter postNotificationName:UIViewControllerRightBarButtonItemsChangedNotification object:self]; } - (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem { if(rightBarButtonItem != nil) [self setRightBarButtonItems:@[ rightBarButtonItem ]]; else [self setRightBarButtonItems:nil]; } @end

ContainerController.m

- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems { _rightBarButtonItems = rightBarButtonItems; [super setRightBarButtonItems:_rightBarButtonItems]; } - (void)setSelectedViewController:(UIViewController *)selectedViewController { if(_selectedViewController != selectedViewController) { if(_selectedViewController != nil) { // Stop listening for right bar button item changed notification on the view controller. NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController]; } _selectedViewController = selectedViewController; if(_selectedViewController != nil) { // Listen for right bar button item changed notification on the view controller. NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(_childRightBarButtonItemsChanged) name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController]; } } } - (void)_childRightBarButtonItemsChanged { NSArray *childRightBarButtonItems = [[_selectedViewController navigationItem] rightBarButtonItems]; NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray:_rightBarButtonItems]; [rightBarButtonItems addObjectsFromArray:childRightBarButtonItems]; [super setRightBarButtonItems:rightBarButtonItems]; }


Sé que este es un tema antiguo, pero acabo de encontrarme con este problema y pensé que alguien más podría hacerlo también.

Así que, para futuras referencias, lo hice de la siguiente manera: envié un bloque al controlador de vista secundario, que simplemente establece el botón derecho del elemento UINavigationItem de los padres. Luego creé un UIBarButtonItem como de costumbre en el controlador de vista secundaria , y llamé a algún método en ese mismo controlador.

Entonces, en ChildViewController.h:

// Declare block property @property (nonatomic, copy) void (^setRightBarButtonBlock)(UIBarButtonItem*);

Y en ChildViewController.m:

self.myBarButton = [[UIBarButtonItem alloc] initWithTitle:@"My Title" style:UIBarButtonItemStylePlain target:self action:@selector(didPressMyBarButton:)]; ... // Show bar button in navigation bar // As normal, just call it with ''nil'' to hide the button if (self.setRightBarButtonBlock) { self.setRightBarButtonBlock(self.myBarButton); } ... - (void)didPressMyBarButton:(UIBarButtonItem *)sender { // Do something here }

Y finalmente en ParentViewController.m

// Initialise child view controller ChildViewController *child = [[ChildViewController alloc] init]; // Give it block for changing bar button item __weak typeof(self) weakSelf = self; child.setRightBarButtonBlock = ^void(UIBarButtonItem *barButtonItem) { [weakSelf.navigationItem setRightBarButtonItem:barButtonItem animated:YES]; }; // Finish the parent-child VC dance

Eso es. Esto me parece bien porque mantiene la lógica correspondiente al UIBarButtonItem en el controlador de vista que está realmente interesado en él.

Nota: Debo mencionar que no soy un profesional. Esta puede ser una forma terrible de hacerlo. Pero parece funcionar bien.