Установите пользовательский подкласс UINavigationBar в UINavigationController программно

92

Кто-нибудь знает, как я могу использовать свой собственный подкласс, UINavigationBarесли я создаю экземпляр UINavigationControllerпрограммно (без IB)?

Перетащите UINavigationControllerв IB, покажите мне панель навигации и используя Identity Inspectory, я могу изменить тип класса и установить свой собственный подкласс, UINavigationBarно программно я не могу, navigationBarсвойство контроллера навигации доступно только для чтения ...

Что мне делать, чтобы программно настроить панель навигации? IB более «мощный», чем «код»? Я считал, что все, что можно сделать в IB, можно сделать и программно.

Дуччо
источник
Вам удалось найти решение где-нибудь еще?
prendio2
вы получите какой-нибудь ответ по этому поводу?
Hrushikesh Betai

Ответы:

89

Вам не нужно гадить с XIB, просто используйте KVC.

[self.navigationController setValue:[[[CustomNavBar alloc]init] autorelease] forKeyPath:@"navigationBar"];
Фред
источник
Это решение работает как шарм (по крайней мере, на iOS 5.1). Все остальные решения кажутся более трудоемкими. Все еще ищу обратную сторону.
Даниэль
6
как вы нашли этот ключевой путь @ "navigationBar" для контроллера навигации. Можете ли вы поделиться этим
Икбал Хан
Вы уверены, что это не приведет к отклонению приложения? На самом деле я нигде не видел этого документированного.
Bani Uppal
Благодарность! Отлично работает и в iOS 6 beta 3! @BaniUppal KVC определенно задокументирован, мы просто используем его с ключом, который немного сложно найти. Я думаю, это главное.
Johannes Lund
9
Это кажется хакерским AF
mattsven
66

Начиная с iOS5, Apple предоставляет способ сделать это напрямую. Справка

UINavigationController *navigationController= [[UINavigationController alloc]initWithNavigationBarClass:[CustomNavBar class] toolbarClass:nil];
[navigationController setViewControllers:[NSArray arrayWithObject:yourRootViewController]];
Паскалий
источник
@nonamelive На самом деле нет, добавлено в iOS6: developer.apple.com/library/ios/#releasenotes/General/…
Паскалиус
7
Да, он добавлен в iOS 6, но он также поддерживается в iOS 5. Инженер Apple упомянул об этом на WWDC 2012, Сессия 216.
nonamelive
37

Начиная с iOS 4, вы можете использовать UINibкласс для решения этой проблемы.

  1. Создайте свой собственный UINavigationBarподкласс.
  2. Создайте пустой xib, добавьте в UINavigationControllerкачестве единственного объекта.
  3. Установите класс для UINavigationController«с UINavigationBarдо вашего пользовательского подкласса.
  4. Настройте контроллер корневого представления одним из следующих способов:
    • [navController setViewcontrollers[NSArray arrayWithObject:myRootVC]];
    • [navController pushViewController:myRootVC];

В коде:

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


Теперь у вас есть UINavigationControllerфайл custom UINavigationBar.

горбстер
источник
Кроме того, как установить rootViewController?
меммонс 06
вы используете [navcontroller setViewControllers: [NSArray arrayWithObject: <YOUR_ROOT_CONTROLLER>]]
coneybeare 08
извините, вы используете вы используете [navcontroller pushViewController: <YOUR_ROOT_CONTROLLER> animated: NO]
coneybeare 08
3
ИМХО, это наименее «хакерский» способ сделать этот иногда неизбежный взлом.
Дэвид Писони
2
Собственно, одна странная проблема. Когда я это делаю, для navigationItem нового navigationController не устанавливается свойство navigationItem, назначенное для viewController, который я помещаю в (пустой) стек.
Дэвид Писони
26

Насколько я могу судить, иногда действительно необходимо создать подкласс UINavigationBar, чтобы провести нестандартный рестайлинг. Иногда можно избежать этого, используя категории , но не всегда.

В настоящее время, насколько мне известно, единственный способ установить пользовательский UINavigationBar в UIViewController - это через IB (то есть через архив) - вероятно, этого не должно быть, но пока мы должны жить с этим.

Часто это нормально, но иногда использование IB нецелесообразно.

Итак, я увидел три варианта:

  1. Подкласс UINavigationBar и подключите все это к IB, а затем возитесь с загрузкой пера каждый раз, когда мне нужен UINavigationController,
  2. Используйте замену метода внутри категории, чтобы изменить поведение UINavigationBar, а не создавать подклассы, или
  3. Подкласс UINavigationBar и немного поработайте с архивацией / разархивированием UINavigationController.

Вариант 1 был для меня в данном случае невозможным (или, по крайней мере, слишком раздражающим), поскольку мне нужно было создать UINavigationController программно, 2 - немного опасный и, на мой взгляд, вариант в крайнем случае, поэтому я выбрал вариант 3.

Мой подход состоял в том, чтобы создать архив «шаблонов» для UINavigationController и разархивировать его, вернув его initWithRootViewController.

Вот как:

В IB я создал UINavigationController с соответствующим набором классов для UINavigationBar.

Затем я взял существующий контроллер и сохранил его архивную копию, используя +[NSKeyedArchiver archiveRootObject:toFile:]. Я только что сделал это в делегате приложения в симуляторе.

Затем я использовал утилиту xxd с флагом -i, чтобы сгенерировать код c из сохраненного файла, чтобы встроить заархивированную версию в свой подкласс ( xxd -i path/to/file).

Внутри initWithRootViewControllerя разархивирую этот шаблон и устанавливаю для себя результат разархивирования:

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

Затем я могу просто взять новый экземпляр моего подкласса UIViewController, в котором установлена ​​настраиваемая панель навигации:

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

Это дает мне модальный UITableViewController с настроенной панелью навигации и панелью инструментов, а также с настраиваемым классом панели навигации. Мне не нужно было делать какую-либо слегка неприятную замену методов, и мне не нужно возиться с перьями, когда я действительно просто хочу работать программно.

Я хотел бы увидеть эквивалент +layerClassUINavigationController +navigationBarClass- но пока это работает.

Майкл Тайсон
источник
5

Я использую "вариант 1"

Создайте nib-файл, содержащий только UINavigationController. И установите для класса UINavigationBar мой собственный класс.

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

[navigationController pushViewController:rootViewController animated:YES];
obb64
источник
так что подходящее имя файла пера будет loadNibNamed: @ "navigationController? Вы фактически получаете весь navigationController из пера, а не только панель. Верно?
aneuryzm
это работает для меня, но когда я нажимаю на любую опцию в моем табличном представлении, я теряю кнопку возврата.
jfisk 02
5

Решение Майкла работает, но вы можете избежать использования NSKeyedArchiver и утилиты xxd. Просто создайте подкласс UINavigationController и переопределите initWithRootViewController, загрузив свой собственный NIB NavigationController напрямую:

- (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 грамм
источник
4

Обновление: использование object_SetClass()больше не работает, как если бы iOS5 GM. Ниже добавлено альтернативное решение.

Используйте NSKeyedUnarchiver, чтобы вручную установить класс разархивирования для панели навигации.

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




Примечание: это оригинальное решение работает только до iOS5:

Есть отличное решение, которое я разместил здесь - вставьте подкласс navBar прямо в ваше представление UINavigationController:

#import <objc/runtime.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    object_setClass(self.navigationController.navigationBar, [MyNavBar class]);
    // the rest of your viewDidLoad code
}
меммоны
источник
Как я указал в предыдущем ответе, вы разместили это - золотой мастер iOS5 сломал это. Однако есть и другие варианты, поэтому я буду редактировать их альтернативным методом.
меммонс
1

Я обнаружил, что в одном из сценариев нам нужно использовать подкласс, а не категорию, это установить цвет фона панели навигации с изображением шаблона, потому что в iOS5 перезапись drawRect с использованием категории больше не работает. Если вы хотите поддерживать ios3.1-5.0, единственный способ сделать это - создать подкласс панели навигации.

Джеймс
источник
1

Эти категориальные методы опасны и не для новичков. Кроме того, сложность, связанная с различием iOS4 и iOS5, делает эту область причиной ошибок для многих людей. Вот простой подкласс, который я использую, который поддерживает iOS4.0 ~ iOS6.0 и очень прост.

.час

@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
Поль де Ланж
источник
0

Это не рекомендуется , чтобы подкласс UINavigationBarкласса. Предпочтительный способ настройки панели навигации - установить ее свойства, чтобы она отображалась так, как вы хотите, и использовать настраиваемые представления внутри UIBarButtonItems вместе с делегатом для получения желаемого поведения.

Что вы пытаетесь сделать, чтобы создать подклассы?

Кроме того, я не думаю, что IB фактически заменяет панель навигации. Я почти уверен, что он просто не отображает панель по умолчанию и имеет вашу настраиваемую панель навигации в качестве подпредставления. Если вы вызовете UINavigationController.navigationBar, получите ли вы экземпляр своей панели?

Бен С
источник
2
Привет, Бен, спасибо. Я знаю, что это не лучший способ создать подкласс UINavigationBar, но я хотел бы установить фоновое изображение, переопределяющее метод drawRect :. Проблема в том, что с помощью IB я могу изменить класс панели навигации в UINavigationController, но программно не могу. И да, IB фактически заменяет панель навигации: NSLog (@ "% @", self.navigationController.navigationBar); <CustomNavigationBar: 0x1806160; baseClass = UINavigationBar; кадр = (0 20; 320 44); clipsToBounds = ДА; непрозрачный = НЕТ; autoresize = W; layer = <CALayer: 0x1806da0 >>
Duccio
Я также использую ту же технику, и она продолжает работать в iOS5 (в отличие от техники категорий UINavigationBar.)
Дэвид Писони
0

Если вы хотите создать подкласс navBar только для изменения фонового изображения - в iOS 5 в этом нет необходимости. Будет такой метод setBackgroundImage

выключен
источник
1
вам нужно использовать, setBackgroundImage:forBarMetrics:как описано здесь: developer.apple.com/library/IOS/#documentation/UIKit/Reference/…
Ахти,
0

В дополнение к комментарию obb64, я закончил тем, что использовал его трюк, setViewControllers:animated:чтобы установить контроллер в качестве контроллера rootControllerдля navigationControllerзагружаемого из пера. Вот код, который я использую:

- (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
источник