Définir une sous-classe personnalisée de UINavigationBar dans UINavigationController par programmation

92

Quelqu'un sait-il comment puis-je utiliser ma sous-classe personnalisée de UINavigationBar si j'instancie par UINavigationControllerprogramme (sans IB)?

Faites glisser un UINavigationControllerdans IB, montrez-moi un sous la barre de navigation et en utilisant Identity Inspectory, je peux changer le type de classe et définir ma propre sous-classe deUINavigationBar mais par programmation je ne peux pas, la navigationBarpropriété de Navigation Controller est en lecture seule ...

Que dois-je faire pour personnaliser la barre de navigation par programme? IB est-il plus "puissant" que "code"? Je pensais que tout ce qui pouvait être fait à l'IB pouvait l'être également par programmation.

Duccio
la source
avez-vous eu de la chance pour trouver une solution ailleurs?
prendio2
avez-vous une réponse à ce sujet?
Hrushikesh Betai

Réponses:

89

Vous n'avez pas besoin de jouer avec le XIB, utilisez simplement KVC.

[self.navigationController setValue:[[[CustomNavBar alloc]init] autorelease] forKeyPath:@"navigationBar"];
Fred
la source
Cette solution fonctionne comme un charme (du moins sur iOS 5.1). Toutes les autres solutions semblent beaucoup plus de travail. Toujours à la recherche de l’inconvénient.
Daniel
6
comment avez-vous trouvé ce chemin clé @ "navigationBar" pour le contrôleur de navigation. pouvez-vous partager ceci
Iqbal Khan
Êtes-vous sûr que cela n'entraînera pas le rejet de l'application? Je n'ai vu cela documenté nulle part en fait.
Bani Uppal
Merci! Fonctionne très bien dans iOS 6 beta 3 aussi! @BaniUppal KVC est certainement documenté, nous l'utilisons simplement avec une clé un peu difficile à trouver. Je pense que c'est le point principal.
Johannes Lund
9
Cela semble hacky AF
Mattsven
66

Depuis iOS5, Apple fournit une méthode pour le faire directement. Référence

UINavigationController *navigationController= [[UINavigationController alloc]initWithNavigationBarClass:[CustomNavBar class] toolbarClass:nil];
[navigationController setViewControllers:[NSArray arrayWithObject:yourRootViewController]];
Pascalius
la source
@nonamelive En fait non, ajouté dans iOS6: developer.apple.com/library/ios/#releasenotes/General/…
Pascalius
7
Oui, il est ajouté dans iOS 6, mais il est également pris en charge dans iOS 5. Un ingénieur Apple l'a mentionné dans la session
216 de la
37

À partir d'iOS 4, vous pouvez utiliser la UINibclasse pour résoudre ce problème.

  1. Créez votre UINavigationBarsous-classe personnalisée .
  2. Créez un xib vide, ajoutez un UINavigationControllercomme objet unique.
  3. Définissez la classe des UINavigationController's UINavigationBarsur votre sous-classe personnalisée.
  4. Définissez votre contrôleur de vue racine via l'une de ces méthodes:
    • [navController setViewcontrollers[NSArray arrayWithObject:myRootVC]];
    • [navController pushViewController:myRootVC];

Dans du code:

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


Vous avez maintenant un UINavigationControlleravec votre coutume UINavigationBar.

gorbster
la source
Sauf alors comment définissez-vous le rootViewController?
memmons
vous utilisez [navcontroller setViewControllers: [NSArray arrayWithObject: <YOUR_ROOT_CONTROLLER>]]
coneybeare
désolé, vous utilisez vous utilisez [navcontroller pushViewController: <YOUR_ROOT_CONTROLLER> animé: NON]
coneybeare
3
À mon humble avis, c'est le moyen le moins «hackish» de faire ce hack parfois inévitable.
David Pisoni
2
En fait, un problème étrange. Lorsque je fais cela, le navigationItem du nouveau navigationController n'est pas défini sur la propriété navigationItem attribuée au viewController que je pousse sur la pile (vide).
David Pisoni
26

Pour autant que je sache, il est parfois en effet nécessaire de sous-classer UINavigationBar, de faire un restyling non standard. Il est parfois possible d'éviter d'avoir à le faire en utilisant des catégories , mais pas toujours.

Actuellement, pour autant que je sache, le seul moyen de définir une UINavigationBar personnalisée dans un UIViewController est via IB (c'est-à-dire via une archive) - cela ne devrait probablement pas être ainsi, mais pour l'instant, nous devons vivre avec.

C'est souvent bien, mais parfois utiliser IB n'est pas vraiment faisable.

Donc, j'ai vu trois options:

  1. Sous-classe UINavigationBar et branchez tout dans IB, puis muck de charger la plume chaque fois que je voulais un UINavigationController,
  2. Utilisez le remplacement de méthode dans une catégorie pour modifier le comportement de UINavigationBar, plutôt que de sous-classer, ou
  3. Sous-classe UINavigationBar et faites un peu de travail avec l'archivage / désarchivage de UINavigationController.

L'option 1 était irréalisable (ou du moins trop ennuyeuse) pour moi dans ce cas, car je devais créer le UINavigationController par programme, 2 est un peu dangereuse et plutôt une option de dernier recours à mon avis, j'ai donc choisi l'option 3.

Mon approche consistait à créer une archive `` modèle '' d'un UINavigationController et à la désarchiver, en la renvoyant dans initWithRootViewController.

Voici comment:

Dans IB, j'ai créé un UINavigationController avec l'ensemble de classes approprié pour UINavigationBar.

Ensuite, j'ai pris le contrôleur existant et en ai enregistré une copie archivée à l'aide de +[NSKeyedArchiver archiveRootObject:toFile:]. Je viens de faire cela dans le délégué de l'application, dans le simulateur.

J'ai ensuite utilisé l'utilitaire 'xxd' avec l'indicateur -i, pour générer du code c à partir du fichier enregistré, pour intégrer la version archivée dans ma sous-classe ( xxd -i path/to/file).

Dans initWithRootViewControllerje désarchive ce modèle et je me fixe au résultat de la désarchivage:

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

Ensuite, je peux simplement récupérer une nouvelle instance de ma sous-classe UIViewController qui a la barre de navigation personnalisée définie:

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

Cela me donne un UITableViewController modal avec une barre de navigation et une barre d'outils configurées, et avec la classe de barre de navigation personnalisée en place. Je n'avais pas besoin de faire de remplacement de méthode légèrement désagréable, et je n'ai pas à me soucier des plumes quand je veux vraiment travailler de manière programmatique.

Je voudrais voir l'équivalent de +layerClassdans UINavigationController - +navigationBarClass- mais pour l'instant, cela fonctionne.

Michael Tyson
la source
5

J'utilise "option 1"

Créez un fichier nib contenant uniquement le UINavigationController. Et définissez la classe UINavigationBar sur ma classe personnalisée.

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

[navigationController pushViewController:rootViewController animated:YES];
obb64
la source
donc le nom approprié du fichier nib serait loadNibNamed: @ "navigationController? Vous obtenez en fait l'intégralité du navigationController de la plume et pas seulement de la barre.
N'est-
cela fonctionne pour moi, mais lorsque je clique sur une option dans mon tableau, je perds le bouton de retour.
jfisk
5

La solution de Michael fonctionne, mais vous pouvez éviter NSKeyedArchiver et l'utilitaire «xxd». Sous-classez simplement UINavigationController et remplacez initWithRootViewController, en chargeant directement votre NIB NavigationController personnalisé:

- (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 grammes
la source
4

Mise à jour: l' utilisation object_SetClass()ne fonctionne plus comme si iOS5 GM. Une solution alternative a été ajoutée ci-dessous.

Utilisez NSKeyedUnarchiver pour définir manuellement la classe de désarchivage de la barre de navigation.

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




Remarque: cette solution originale ne fonctionne que pré-iOS5:

Il existe une excellente solution, que j'ai publiée ici - injectez la sous-classe navBar directement dans votre vue UINavigationController:

#import <objc/runtime.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    object_setClass(self.navigationController.navigationBar, [MyNavBar class]);
    // the rest of your viewDidLoad code
}
Memmons
la source
Comme je l'ai indiqué dans la réponse précédente sur laquelle vous avez posté ceci, le maître d'or d'iOS5 a cassé cela. Il y a d'autres options disponibles cependant, donc je vais éditer avec une méthode alternative.
memmons
1

Un scénario que j'ai trouvé que nous devons utiliser la sous-classe plutôt que la catégorie consiste à définir la couleur d'arrière-plan de la barre de navigation avec l'image du motif, car dans iOS5, l'écrasement de drawRect en utilisant la catégorie ne fonctionne plus. Si vous souhaitez prendre en charge ios3.1-5.0, la seule façon de le faire est de sous-classer la barre de navigation.

James
la source
1

Ces méthodes de catégorie sont dangereuses et ne conviennent pas aux novices. De plus, la complication avec iOS4 et iOS5 étant différents, en fait un domaine qui peut causer des bogues pour de nombreuses personnes. Voici une sous-classe simple que j'utilise qui prend en charge iOS4.0 ~ iOS6.0 et qui est très simple.

.h

@interface XXXNavigatioNBar : UINavigationBar
@end

.m

#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
la source
0

Il n'est pas recommandé de sous- UINavigationBarclasser la classe. La meilleure façon de personnaliser la barre de navigation est de définir ses propriétés pour la faire apparaître comme vous le souhaitez et d'utiliser des vues personnalisées dans UIBarButtonItems avec un délégué pour obtenir le comportement souhaité.

Qu'essayez-vous de faire qui nécessite un sous-classement?

De plus, je ne pense pas qu'IB remplace réellement la barre de navigation. Je suis presque sûr qu'il n'affiche tout simplement pas celui par défaut et que votre barre de navigation personnalisée est une sous-vue. Si vous appelez UINavigationController.navigationBar, obtenez-vous une instance de votre barre?

Ben S
la source
2
Salut Ben, merci. Je sais que ce n'est pas le meilleur moyen de sous-classer UINavigationBar mais je voudrais configurer une image d'arrière-plan remplaçant drawRect: method. Le problème est qu'en utilisant IB, je peux changer la classe de la barre de navigation dans un UINavigationController, mais je ne peux pas par programme. Et oui, IB remplace en fait la barre de navigation: NSLog (@ "% @", self.navigationController.navigationBar); <CustomNavigationBar: 0x1806160; baseClass = UINavigationBar; cadre = (0 20; 320 44); clipsToBounds = OUI; opaque = NON; autoresize = W; layer = <CALayer: 0x1806da0 >>
Duccio
J'utilise également cette même technique, et elle continue de fonctionner dans iOS5 (contrairement à la technique de catégorie UINavigationBar.)
David Pisoni
0

Suite au commentaire de obb64, j'ai fini par utiliser son astuce avec setViewControllers:animated:pour définir le contrôleur comme rootControllerpour le navigationControllerchargé à partir de la pointe. Voici le code que j'utilise:

- (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
la source