Establecer una subclase personalizada de UINavigationBar en UINavigationController mediante programación

92

¿Alguien sabe cómo puedo usar mi subclase personalizada UINavigationBarsi creo una instancia mediante UINavigationControllerprogramación (sin IB)?

Arrastra un UINavigationControlleren IB y muéstrame debajo de la barra de navegación y usando el Inspector de identidad puedo cambiar el tipo de clase y establecer mi propia subclase, UINavigationBarpero programáticamente no puedo, la navigationBarpropiedad del controlador de navegación es de solo lectura ...

¿Qué debo hacer para personalizar la barra de navegación mediante programación? ¿IB es más "poderoso" que el "código"? Creía que todo lo que se puede hacer en IB también se puede hacer mediante programación.

Duccio
fuente
¿Tuviste suerte para encontrar una solución en otro lugar?
prendio2
¿Obtienes alguna respuesta al respecto?
Hrushikesh Betai

Respuestas:

89

No es necesario que juegue con el XIB, simplemente use KVC.

[self.navigationController setValue:[[[CustomNavBar alloc]init] autorelease] forKeyPath:@"navigationBar"];
Fred
fuente
Esta solución funciona a las mil maravillas (al menos en iOS 5.1). Todas las demás soluciones parecen mucho más laboriosas. Sigo buscando la desventaja.
Daniel
6
¿Cómo encontraste esta ruta clave @ "navigationBar" para el controlador de navegación? ¿Puedes compartir esto?
Iqbal Khan
¿Estás seguro de que esto no resultará en el rechazo de la aplicación? En realidad, no vi esto documentado en ninguna parte.
Bani Uppal
¡Gracias! ¡Funciona muy bien en iOS 6 beta 3 también! @BaniUppal KVC ciertamente está documentado, solo lo estamos usando con una clave que es un poco difícil de encontrar. Creo que ese es el punto principal.
Johannes Lund
9
Esto parece hacky AF
mattsven
66

Desde iOS5, Apple proporciona un método para hacer esto directamente. Referencia

UINavigationController *navigationController= [[UINavigationController alloc]initWithNavigationBarClass:[CustomNavBar class] toolbarClass:nil];
[navigationController setViewControllers:[NSArray arrayWithObject:yourRootViewController]];
Pascalio
fuente
@nonamelive En realidad no, agregado en iOS6: developer.apple.com/library/ios/#releasenotes/General/…
Pascalius
7
Sí, se agregó en iOS 6, pero también es compatible con iOS 5. Un ingeniero de Apple mencionó eso en la Sesión 216 de la WWDC 2012.
finalizado el
37

A partir de iOS 4, puede usar la UINibclase para ayudar a resolver este problema.

  1. Crea tu UINavigationBarsubclase personalizada .
  2. Cree un xib vacío, agregue un UINavigationControllercomo único objeto.
  3. Establecer la clase de los UINavigationController's UINavigationBarpara su subclase personalizada.
  4. Configure su controlador de vista raíz a través de uno de estos métodos:
    • [navController setViewcontrollers[NSArray arrayWithObject:myRootVC]];
    • [navController pushViewController:myRootVC];

En codigo:

UINib *nib = [UINib nibWithNibName:@"YourCustomXib" bundle:nil];
UINavigationController *navController = 
             [[nib instantiateWithOwner:nil options:nil] objectAtIndex:0];


Ahora tienes una UINavigationControllercon tu costumbre UINavigationBar.

gorbster
fuente
Excepto entonces, ¿cómo se configura el rootViewController?
memmons
utiliza [navcontroller setViewControllers: [NSArray arrayWithObject: <YOUR_ROOT_CONTROLLER>]]
coneybeare
lo siento, usas usas [navcontroller pushViewController: <YOUR_ROOT_CONTROLLER> animado: NO]
coneybeare
3
En mi humilde opinión, esta es la forma menos "pirateada" de hacer este truco a veces inevitable.
David Pisoni
2
En realidad, un problema extraño. Cuando hago esto, el navigationItem del nuevo navigationController no se establece en la propiedad navigationItem asignada al viewController que estoy insertando en la pila (vacía).
David Pisoni
26

Por lo que puedo decir, a veces es necesario subclasificar UINavigationBar, para hacer un rediseño no estándar. A veces es posible evitar tener que hacerlo mediante el uso de categorías , pero no siempre.

Actualmente, hasta donde yo sé, la única forma de configurar un UINavigationBar personalizado dentro de un UIViewController es a través de IB (es decir, a través de un archivo); probablemente no debería ser así, pero por ahora, debemos vivir con él.

Esto suele estar bien, pero a veces utilizar IB no es realmente factible.

Entonces, vi tres opciones:

  1. Subclase UINavigationBar y conéctelo todo en IB, luego ensucie la carga de la punta cada vez que quiera un UINavigationController,
  2. Use el reemplazo de método dentro de una categoría para cambiar el comportamiento de UINavigationBar, en lugar de subclasificar, o
  3. Subclase UINavigationBar y haga un poco de trabajo con archivar / desarchivar el UINavigationController.

La opción 1 fue inviable (o al menos demasiado molesta) para mí en este caso, ya que necesitaba crear el UINavigationController programáticamente, 2 es un poco peligrosa y más una opción de último recurso en mi opinión, así que elegí la opción 3.

Mi enfoque fue crear un archivo de 'plantilla' de un UINavigationController, y desarchivarlo, devolviéndolo en formato initWithRootViewController.

Así es cómo:

En IB, creé un UINavigationController con el conjunto de clases apropiado para UINavigationBar.

Luego, tomé el controlador existente y guardé una copia archivada usando +[NSKeyedArchiver archiveRootObject:toFile:]. Acabo de hacer esto dentro del delegado de la aplicación, en el simulador.

Luego utilicé la utilidad 'xxd' con el indicador -i, para generar código c desde el archivo guardado, para incrustar la versión archivada en mi subclase ( xxd -i path/to/file).

Dentro initWithRootViewControllerdesarchivo esa plantilla, y me pongo al resultado del desarchivar:

// This is the data from [NSKeyedArchiver archivedDataWithRootObject:controller], where
// controller is a CTNavigationController with navigation bar class set to CTNavigationBar,
// from IB.  This c code was created using 'xxd -i'
static unsigned char archived_controller[] = {
    0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd4, 0x01, 0x02, 0x03,
    ...
};
static unsigned int archived_controller_len = 682;

...

- (id)initWithRootViewController:(UIViewController *)rootViewController {
     // Replace with unarchived view controller, necessary for the custom navigation bar
     [self release];
     self = (CTNavigationController*)[NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithBytes:archived_controller length:archived_controller_len]];
     [self setViewControllers:[NSArray arrayWithObject:rootViewController]];
     return [self retain];
}

Luego, puedo simplemente tomar una nueva instancia de mi subclase UIViewController que tiene configurada la barra de navegación personalizada:

UIViewController *modalViewController = [[[CTNavigationController alloc] initWithRootViewController:myTableViewController] autorelease];
[self.navigationController presentModalViewController:modalViewController animated:YES];

Esto me da un UITableViewController modal con una barra de navegación y una barra de herramientas configuradas, y con la clase de barra de navegación personalizada en su lugar. No tuve que hacer ningún reemplazo de método ligeramente desagradable, y no tengo que perder el tiempo con las puntas cuando realmente solo quiero trabajar programáticamente.

Me gustaría ver el equivalente de +layerClassdentro de UINavigationController +navigationBarClass, pero por ahora esto funciona.

Michael Tyson
fuente
5

Yo uso la "opción 1"

Cree un archivo nib que solo contenga UINavigationController. Y establezca la clase UINavigationBar en mi clase personalizada.

self.navigationController = [[[NSBundle mainBundle] loadNibNamed:@"navigationbar" owner:self options:nil] lastObject];

[navigationController pushViewController:rootViewController animated:YES];
obb64
fuente
por lo que el nombre apropiado del archivo nib sería loadNibNamed: @ "navigationController? En realidad, está obteniendo el navigationController completo de la punta y no solo de la barra. ¿
Verdad
esto funciona para mí, pero cuando hago clic en cualquier opción en mi vista de tabla, pierdo el botón Atrás.
jfisk
5

La solución de Michael funciona, pero puede evitar NSKeyedArchiver y la utilidad 'xxd'. Simplemente subclase UINavigationController y anule initWithRootViewController, cargando su NIB NavigationController personalizado directamente:

- (id) initWithRootViewController:(UIViewController *)rootViewController
{
    [self release];
    self = [[[[NSBundle mainBundle] loadNibNamed:@"CTNavigationController" owner:nil options:nil] objectAtIndex:0] retain];  
    [self setViewControllers:[NSArray arrayWithObject:rootViewController]];
    return self;
}
100 gramos
fuente
4

Actualización: el uso object_SetClass()ya no funciona como si iOS5 GM. Se ha agregado una solución alternativa a continuación.

Use NSKeyedUnarchiver para configurar manualmente la clase de desarchivar para la barra de navegación.

   MyViewController *controller = [[[MyViewController alloc] init] autorelease];
   NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[NSKeyedArchiver archivedDataWithRootObject:controller]] autorelease];
   [unarchiver setClass:[MyNavigationBar class] forClassName:@"UINavigationBar"];
   controller = [unarchiver decodeObjectForKey:@"root"];




Nota: esta solución original solo funciona antes de iOS5:

Hay una gran solución, que publiqué aquí : inyecte la subclase navBar directamente en su vista UINavigationController:

#import <objc/runtime.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    object_setClass(self.navigationController.navigationBar, [MyNavBar class]);
    // the rest of your viewDidLoad code
}
memmons
fuente
Como indiqué en la respuesta anterior, publicaste esto: el maestro de oro de iOS5 rompió esto. Sin embargo, hay otras opciones disponibles, así que editaré con un método alternativo.
memmons
1

Un escenario que encontré en el que necesitamos usar una subclase en lugar de una categoría es establecer el color de fondo de la barra de navegación con la imagen del patrón, porque en iOS5 sobrescribir drawRect usando la categoría ya no funciona. Si desea admitir ios3.1-5.0, la única forma de hacerlo es subclasificar la barra de navegación.

James
fuente
1

Estos métodos de categoría son peligrosos y no para principiantes. Además, la complicación con iOS4 e iOS5 son diferentes, lo que hace que esta sea un área que puede causar errores para muchas personas. Aquí hay una subclase simple que uso que es compatible con iOS4.0 ~ iOS6.0 y es muy simple.

.h

@interface XXXNavigatioNBar : UINavigationBar
@end

.metro

#import "XXXNavigationBar.h"

#import <objc/runtime.h>

@implementation XXXNavigationBar

- (void) didMoveToSuperview {
    if( [self respondsToSelector: @selector(setBackgroundImage:forBarMetrics:)]) {
        //iOS5.0 and above has a system defined method -> use it
        [self setBackgroundImage: [UIImage imageNamed: @"nav-bar"]
                   forBarMetrics: UIBarMetricsDefault];
    }
    else {
        //iOS4.0 requires us to override drawRect:. BUT!!
        //If you override drawRect: on iOS5.0 the system default will break,
        //so we dynamically add this method if required
        IMP implementation = class_getMethodImplementation([self class], @selector(iOS4drawRect:));
        class_addMethod([self class], @selector(drawRect:), implementation, "v@:{name=CGRect}");
    }
}

- (void)iOS4drawRect: (CGRect) rect {
    UIImage* bg = [UIImage imageNamed:@"nav-bar-blue"];
    [bg drawInRect: rect];
}

@end
Paul de Lange
fuente
0

No se recomienda subclasificar la UINavigationBarclase. La forma preferida de personalizar la barra de navegación es establecer sus propiedades para que aparezca como desee y usar vistas personalizadas dentro de UIBarButtonItems junto con un delegado para obtener el comportamiento deseado.

¿Qué estás intentando hacer que necesite subclases?

Además, no creo que IB esté reemplazando la barra de navegación. Estoy bastante seguro de que simplemente no muestra el predeterminado y tiene su barra de navegación personalizada como una subvista. Si llama a UINavigationController.navigationBar, ¿obtiene una instancia de su barra?

Ben S
fuente
2
Hola Ben, gracias. Sé que no es la mejor manera de subclase UINavigationBar, pero me gustaría configurar una imagen de fondo que anule el método drawRect:. El problema es que al usar IB puedo cambiar la clase de la barra de navegación en un UINavigationController pero programáticamente no puedo. Y sí, IB está reemplazando la barra de navegación: NSLog (@ "% @", self.navigationController.navigationBar); <CustomNavigationBar: 0x1806160; baseClass = UINavigationBar; marco = (0 20; 320 44); clipsToBounds = SÍ; opaco = NO; autoresize = W; layer = <CALayer: 0x1806da0 >>
Duccio
También estoy usando esta misma técnica, y continúa funcionando en iOS5 (a diferencia de la técnica de categoría UINavigationBar)
David Pisoni
0

Además del comentario de obb64, terminé usando su truco con setViewControllers:animated:para configurar el controlador como rootControllerel navigationControllercargado desde la punta. Aquí está el código que estoy usando:

- (void) presentModalViewControllerForClass: (Class) a_class {
  UINavigationController *navController = [[[NSBundle mainBundle] loadNibNamed: @"CustomNavBar" owner:self options:nil] lastObject];

  LoginSignupBaseViewController *controller = [[a_class alloc] initWithNibName: nil bundle: nil];
  controller.navigationController = navController;
  [navController setViewControllers: A(controller) animated: NO];

  [self presentModalViewController: navController animated: YES];

  [controller release];
}
smtlaissezfaire
fuente