Defina uma subclasse personalizada de UINavigationBar em UINavigationController de forma programática

92

Alguém sabe como posso usar minha subclasse customizada de UINavigationBarse eu instanciar UINavigationControllerprogramaticamente (sem IB)?

Arraste um UINavigationControllerno IB para me mostrar um abaixo da barra de navegação e usando o Identity Inspectory posso alterar o tipo de classe e definir minha própria subclasse de, UINavigationBarmas programaticamente não consigo, a navigationBarpropriedade do Navigation Controller é somente leitura ...

O que devo fazer para personalizar a barra de navegação de forma programática? O IB é mais "poderoso" do que "código"? Eu acreditava que tudo o que pode ser feito no IB também pode ser feito de forma programática.

Duccio
fonte
você teve alguma sorte em encontrar uma solução em outro lugar?
prendio2
você tem alguma resposta sobre isso?
Hrushikesh Betai

Respostas:

89

Você não precisa mexer com o XIB, basta usar KVC.

[self.navigationController setValue:[[[CustomNavBar alloc]init] autorelease] forKeyPath:@"navigationBar"];
Fred
fonte
Esta solução funciona perfeitamente (pelo menos no iOS 5.1). Todas as outras soluções parecem funcionar muito mais. Ainda procurando o lado negativo.
Daniel,
6
como você encontrou este caminho-chave @ "navigationBar" para o controlador de navegação. você pode compartilhar isto
Iqbal Khan
Tem certeza de que isso não resultará na rejeição do aplicativo? Na verdade, não vi isso documentado em lugar nenhum.
Bani Uppal
Obrigado! Funciona muito bem no iOS 6 beta 3 também! @BaniUppal KVC certamente está documentado, estamos apenas usando-o com uma chave um pouco difícil de encontrar. Acho que esse é o ponto principal.
Johannes Lund
9
Isso parece hacky AF
mattsven
66

Desde o iOS5, a apple fornece um método para fazer isso diretamente. Referência

UINavigationController *navigationController= [[UINavigationController alloc]initWithNavigationBarClass:[CustomNavBar class] toolbarClass:nil];
[navigationController setViewControllers:[NSArray arrayWithObject:yourRootViewController]];
Pascalius
fonte
@nonamelive Na verdade não, adicionado ao iOS6: developer.apple.com/library/ios/#releasenotes/General/…
Pascalius
7
Sim, é adicionado no iOS 6, mas também é compatível com o iOS 5. Um engenheiro da Apple mencionou isso na WWDC 2012 Session 216.
nonamelive
37

A partir do iOS 4, você pode usar a UINibclasse para ajudar a resolver esse problema.

  1. Crie sua UINavigationBarsubclasse personalizada .
  2. Crie um xib vazio e adicione a UINavigationControllercomo o único objeto.
  3. Defina a classe de UINavigationController's UINavigationBarpara sua subclasse personalizada.
  4. Defina seu controlador de visualização raiz por meio de um destes métodos:
    • [navController setViewcontrollers[NSArray arrayWithObject:myRootVC]];
    • [navController pushViewController:myRootVC];

Em código:

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


Agora você tem um UINavigationControllercom o seu costume UINavigationBar.

gorbster
fonte
Exceto então como você define o rootViewController?
memmons
você usa [navcontroller setViewControllers: [NSArray arrayWithObject: <YOUR_ROOT_CONTROLLER>]]
coneybeare
desculpe, você usa [navcontroller pushViewController: <YOUR_ROOT_CONTROLLER> animado: NÃO]
coneybeare
3
IMHO, esta é a maneira menos "hackeada" aqui de fazer esse hack às vezes inevitável.
David Pisoni
2
Na verdade, um problema estranho. Quando faço isso, o navigationItem do novo navigationController não está sendo definido para a propriedade navigationItem atribuída ao viewController que estou colocando na pilha (vazia).
David Pisoni
26

Pelo que eu posso dizer, às vezes é realmente necessário subclasse UINavigationBar, para fazer algum restyling fora do padrão. Às vezes, é possível evitar ter que fazer isso usando categorias , mas nem sempre.

Atualmente, até onde eu sei, a única maneira de definir um UINavigationBar personalizado dentro de um UIViewController é via IB (ou seja, por meio de um arquivo) - provavelmente não deveria ser assim, mas por enquanto, temos que conviver com isso.

Isso geralmente é bom, mas às vezes o uso de IB não é realmente viável.

Então, eu vi três opções:

  1. Subclasse UINavigationBar e conecte tudo em IB, então muck sobre como carregar o bico cada vez que eu queria um UINavigationController,
  2. Use a substituição de método dentro de uma categoria para alterar o comportamento de UINavigationBar, ao invés de subclassificação, ou
  3. Subclasse UINavigationBar e faça uma pequena bagunça arquivando / desarquivando o UINavigationController.

A opção 1 era inviável (ou pelo menos chata demais) para mim neste caso, pois eu precisava criar o UINavigationController programaticamente, 2 é um pouco perigoso e mais uma opção de último recurso na minha opinião, então escolhi a opção 3.

Minha abordagem foi criar um arquivo de 'modelo' de um UINavigationController e desarquivá-lo, devolvendo-o initWithRootViewController.

Veja como:

No IB, criei um UINavigationController com a classe apropriada definida para o UINavigationBar.

Então, peguei o controlador existente e salvei uma cópia arquivada dele usando +[NSKeyedArchiver archiveRootObject:toFile:]. Acabei de fazer isso dentro do delegado do aplicativo, no simulador.

Em seguida, usei o utilitário 'xxd' com o sinalizador -i, para gerar o código c do arquivo salvo, para incorporar a versão arquivada em minha subclasse ( xxd -i path/to/file).

Dentro, initWithRootViewControllereu desarquivo esse modelo e me coloco como o resultado do desarquivamento:

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

Então, posso simplesmente pegar uma nova instância da minha subclasse UIViewController que tem a barra de navegação personalizada definida:

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

Isso me dá um UITableViewController modal com uma barra de navegação e barra de ferramentas configuradas, e com a classe de barra de navegação personalizada no lugar. Não precisei fazer nenhuma substituição de método um pouco desagradável e não tenho que me preocupar com pontas quando na verdade quero apenas trabalhar programaticamente.

Eu gostaria de ver o equivalente de +layerClassdentro de UINavigationController - +navigationBarClass- mas, por enquanto, isso funciona.

Michael Tyson
fonte
5

Eu uso a "opção 1"

Crie um arquivo nib com apenas o UINavigationController nele. E defina a classe UINavigationBar para minha classe personalizada.

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

[navigationController pushViewController:rootViewController animated:YES];
obb64
fonte
então o nome apropriado do arquivo nib seria loadNibNamed: @ "navigationController? Na verdade, você está obtendo todo o navigationController do nib e não apenas a barra. Certo?
aneuryzm
isso funciona para mim, mas quando clico em qualquer opção no tableview, perco o botão Voltar.
jfisk
5

A solução de Michael funciona, mas você pode evitar o NSKeyedArchiver e o utilitário 'xxd'. Simplesmente subclasse UINavigationController e substitua initWithRootViewController, carregando seu NIB de NavigationController personalizado diretamente:

- (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 gramas
fonte
4

Atualização: Usar object_SetClass()não funciona mais como se iOS5 GM. Uma solução alternativa foi adicionada abaixo.

Use NSKeyedUnarchiver para definir manualmente a classe de desarquivamento para a barra de navegação.

   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 solução original só funciona antes do iOS5:

Há uma ótima solução, que postei aqui - injete a subclasse navBar diretamente em sua visualização UINavigationController:

#import <objc/runtime.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    object_setClass(self.navigationController.navigationBar, [MyNavBar class]);
    // the rest of your viewDidLoad code
}
memmons
fonte
Como indiquei na resposta anterior, você postou isso no - o master gold do iOS5 quebrou isso. No entanto, existem outras opções disponíveis, então vou editar com um método alternativo.
memmons
1

Um cenário que descobri que precisamos usar subclasse em vez de categoria é definir a cor de fundo da barra de navegação com a imagem padrão, porque no iOS5 a substituição de drawRect usando categoria não funciona mais. Se você deseja oferecer suporte a ios3.1-5.0, a única maneira de fazer é criar uma subclasse da barra de navegação.

James
fonte
1

Esses métodos de categoria são perigosos e não para iniciantes. Além disso, a complicação de iOS4 e iOS5 serem diferentes, torna esta uma área que pode causar bugs para muitas pessoas. Aqui está uma subclasse simples que eu uso que oferece suporte a iOS4.0 ~ iOS6.0 e é muito simples.

.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
fonte
0

Não é recomendado criar uma subclasse da UINavigationBarclasse. A maneira preferida de personalizar a barra de navegação é definir suas propriedades para fazê-la aparecer como você deseja e usar visualizações personalizadas dentro de UIBarButtonItems junto com um delegado para obter o comportamento desejado.

O que você está tentando fazer que precisa de uma subclasse?

Além disso, não acho que o IB esteja realmente substituindo a barra de navegação. Tenho certeza que ele simplesmente não está exibindo o padrão e tem sua barra de navegação personalizada como uma subvisualização. Se você chamar UINavigationController.navigationBar, você obtém uma instância de sua barra?

Ben S
fonte
2
Oi Ben, obrigado. Eu sei que não é a melhor maneira de criar uma subclasse de UINavigationBar, mas gostaria de configurar uma imagem de fundo substituindo o método drawRect :. O problema é que usando IB posso mudar a classe da barra de navegação em um UINavigationController, mas programaticamente não consigo. E sim, o IB está na verdade substituindo a barra de navegação: NSLog (@ "% @", self.navigationController.navigationBar); <CustomNavigationBar: 0x1806160; baseClass = UINavigationBar; frame = (0 20; 320 44); clipsToBounds = YES; opaco = NÃO; redimensionar automaticamente = W; camada = <CALayer: 0x1806da0 >>
Duccio
Também estou usando essa mesma técnica e ela continua a funcionar no iOS5 (ao contrário da técnica da categoria UINavigationBar.)
David Pisoni
0

Na sequência do comentário de obb64, acabei usando seu truque com setViewControllers:animated:para definir o controlador como o rootControllerpara o navigationControllercarregado da ponta. Este é o código que estou 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
fonte