Back button in standalone UINavigationBar with UINavigationController - ios

I am trying to implement a push-up UINavigationBar, where the position of the navigation bar is attached to the contentOffset of the UIScrollView (similar to how safari works in ios7).
In order to get the dynamic movement working I am using a UINavigationBar created programatically and added as a subview of the UIViewController's view (it is accessible as self.navbar).
The UIViewController is within a UINavigationController hierarchy, so I am hiding the built-in self.navigationController.navigationBar at the top of -viewWillAppear:.
The problem I am trying to solve is to add a back button to this new standalone navbar. I would preferably like to simply copy the buttons or even the navigationItems from the navigationController and its hidden built-in navbar, but this doesnt seem to work
Is my only solution to set leftBarButtonItem on my standalone navbar to be a fake back button (when there is a backItem in the navController's navbar)? This seems a bit hacky, and I'd rather use the built backButton functionality.

Another way to do that, once you have your own UINavigationBar set, is to push two UINavigationItems on your navigationBar, causing back button to appear. You can then customize what happens when the back button is pressed.
Here's how I did that
1 - Some UINavigationItem subclass, to define extra-behavior / customization parameters
#interface MyNavigationItem : UINavigationItem
//example : some custom back action when 'back' is pressed
#property (nonatomic, copy) void (^onBackClickedAction)(void);
#end
2 - Then wire that into your UINavigationBarDelegate :
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
if ([item isKindOfClass:[MyNavigationItem class]]) {
MyNavigationItem *navItem = (MyNavigationItem *)item;
//custom action
if (navItem.backAction) {
navItem.backAction();
}
return YES;// return NO if you don't want your bar to animate to previous item
} else {
return YES;
}
}
You could adapt that scheme, calling your UINavigationController pop method on back action.
This is still hacky

Vinzzz' answer was a good solution. Here is my implementation, as the context was slightly different.
In the UIViewController's viewDidLoad method I setup my navbar's navigation items like this:
NSMutableArray* navItems = [#[] mutableCopy];
if (self.navigationController.viewControllers.count > 1)
{
NSInteger penultimateIndex = (NSInteger)self.navigationController.viewControllers.count - 2;
UIViewController* prevVC = (penultimateIndex >= 0) ? self.navigationController.viewControllers[penultimateIndex] : nil;
UINavigationItem* prevNavItem = [[UINavigationItem alloc] init];
prevNavItem.title = prevVC.title;
[navItems addObject:prevNavItem];
}
UINavigationItem* currNavItem = [[UINavigationItem alloc] init];
... <Add any other left/right buttons to the currNavItem> ...
[navItems addObject:currNavItem];
[self.navbar setItems:navItems];
...where self.navbar is my floating stand-alone UINavigationBar.
I also assign the current view controller to be self.navbar's delegate, and then listen for the -navigationBar:shouldPopItem: event that is triggered when the back button is pressed:
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
if (navigationBar == self.navbar)
{
[self.navigationController popViewControllerAnimated:YES];
return NO;
}
return YES;
}
(If you return YES, it will crash when a swipe gesture is used in ios7).

Related

PopViewController strange behaviour

Due to a weird request which I tried to turn down but it didn't work, I had to override the navigationBar's back Button.
I have made a custom UINavigationController subclass and hacked the
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item method.
Here is my code:
#interface CustomUINavigationController ()
#end
#implementation CustomUINavigationController
#pragma mark - UINavigationBar delegate methods
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
[self popViewControllerAnimated:YES];
return YES;
} else {
return NO;
}
}
[self popViewControllerAnimated:YES];
return YES;
}
#end
All works fine, except when I pop a viewController programmatically. The app crashed every time when I wanted to perform a push after said pop. Turning NSZombie on, revealed that when popping a viewController programmatically, its parent viewController is deallocated.
At this point, making a custom backButton is not a option since it will lose the native iOS 7 swipe to popViewController feature.
Crash log:
*** -[ContactsDetailViewController performSelector:withObject:withObject:]: message sent to deallocated instance 0x1806b790
(My previous post was completely wrong. This is a complete rewrite with an appropriate solution.)
I had this behavior pop up when I chose to delete some code generating a warning when I was converting to ARC -- code that I thought was not being called.
Here's the situation:
If you shadow navigationBar:shouldPopItem: in a subclass of UINavigationController, then the current view controller will NOT be popped when the user touches the NavBar's BACK button. However, if you call popViewControllerAnimated: directly, your navigationBar:shouldPopItem: will still be called, and the view controller will pop.
Here's why the view controller fails to pop when the user touches the BACK button:
UINavigationController has a hidden method called navigationBar:shouldPopItem:. This method IS called when the user clicks the BACK button, and it is the method that normally calls popViewControllerAnimated: when the user touches the BACK button.
When you shadow navigationBar:shouldPopItem:, the super class' implementation is not called, and hence the ViewController is not popped.
Why you should NOT call popViewControllerAnimated: within your subclass' navigationBar:shouldPopItem::
If you call popViewControllerAnimated: within navigationBar:shouldPopItem:, you will see the behavior that you desire when you click the BACK button on the NavBar: You can determine whether or not you want to pop, and your view controller pops if you want it to.
But, if you call popViewControllerAnimated: directly, you will end up popping two view controllers: One from your direct call to popViewControllerAnimated:, and one from the call you added to within navigationBar:shouldPopItem:.
What I believe to be the safe solution:
Your custom nav controller should be declared like this:
#interface CustomNavigationController : UINavigationController <UINavigationBarDelegate>
{
// .. any ivars you want
}
#end
Your implementation should contain code that looks something like this:
// Required to prevent a warning for the call [super navigationBar:navigationBar shouldPopItem:item]
#interface UINavigationController () <UINavigationBarDelegate>
#end
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
BOOL rv = TRUE;
if ( /* some condition to determine should NOT pop */ )
{
// we won't pop
rv = FALSE;
// extra code you might want to execute ...
} else
{
// It's not documented that the super implements this method, so we're being safe
if ([[CustomNavigationController superclass]
instancesRespondToSelector:#selector(navigationBar:shouldPopItem:)])
{
// Allow the super class to do its thing, which includes popping the view controller
rv = [super navigationBar:navigationBar shouldPopItem:item];
}
}
return rv;
}
I'm not 100% certain but I don't think you should actually be popping the view controller in that delegate method.
"should" delegate methods don't normally do something. They just assert whether something should or shouldn't be done.
Change your method to this...
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
return YES;
} else {
return NO;
}
}
return YES;
}
And see if it works.
All I have done is removed the popViewController calls.
EDIT - How to add a custom back button
In a category on UIBarButtonItem...
+ (UIBarButtonItem *)customBackButtonWithTarget:(id)target action:(#SEL)action
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"Some image"] forState:UIControlStateNormal];
[button setTitle:#"Some Title" forState:UIControlStateNormal];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
return barButtonItem;
}
Now whenever you want to set a custom back button just use...
UIBarButtonItem *backButton = [UIBarButtonItem customBackButtonWithTarget:self action:#selector(backButtonPressed)];
I would suggest a completely different approach.
Create a base class for the view controllers that you are pushing on the navigation stack. In the viewDidLoad method set your custom button as the leftBarButtonItem of the navigationItem and add a -backAction: which invokes the popViewControllerAnimated: method of the navigation controller.
That way you won't care about things like losing functionality of UINavigationController like the swipe to pop and you won't have to override the navigationBar:shouldPopItem: method at all.
You probably need to do [super shouldPop... instead of actual [self popViewControllerAnimated:YES];.
The reason being that the way UINavigationController implements stack is private, so you should mess with the method calls as little as possible.
Anyway, this looks like a hack. Moreover, the user will have no visual clue that you are blocking the navigation action. What's wrong with disabling the button via:
self.navigationController.navigationItem.backBarButtonItem.enabled = NO;
It's my fix to #henryaz answer for Xcode 11:
#interface UINavigationControllerAndNavigationBarDelegate : UINavigationController<UINavigationBarDelegate>
#end
#interface CustomNavigationController : UINavigationControllerAndNavigationBarDelegate
#end
// changed this method just a bit
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
BOOL shouldPop = // detect if need to pop
if (shouldPop) {
shouldPop = [super navigationBar:navigationBar shouldPopItem:item]; // before my fix this code failed with compile error
}
return shouldPop;
}

UINavigationBar only calling some delegates

I'm adding a UINavigationItem with a save button to the right side, and a back button on the left side to my UINavigationBar. I am setting my view controller to be the delegate, and the navigation bar is calling some delegate functions, but not -(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item.
In ISStudioReviewViewController.h:
#interface ISStudioReviewViewController : UIViewController <UINavigationBarDelegate, UITextViewDelegate>
#property (weak, nonatomic) IBOutlet UINavigationBar *header;
#end
In ISStudioReviewViewController.m:
-(void)viewDidLoad {
[super viewDidLoad];
...
...
UINavigationItem *navHeader = [[UINavigationItem alloc] initWithTitle:#"Compose Review"];
[navHeader setHidesBackButton:NO];
UIBarButtonItem *actionButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave
target:self
action:#selector(saveReview)];
[navHeader setRightBarButtonItem:actionButton];
[header setDelegate:self];
[header pushNavigationItem:navHeader animated:NO];
}
-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
NSLog(#"Popping item");
....
return YES;
}
-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPushItem:(UINavigationItem *)item
{
NSLog(#"Should push item");
return YES;
}
-(void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item
{
NSLog(#"Pushing item!");
}
In my logs I can see that the shouldPush and didPush delegate methods are being called correctly, and my back button is being displayed. However clicking on the back button is not calling the delegate method, and Popping item is never logged.
I should note that the save button actionButton does properly call it's target without issue. I have tried not adding this button, and seeing if that is somehow causing the back button to fail, but no luck.
I am using ARC and targeting iOS 6.0.
The Apple Documentation states that you are not allowed to do this delegate.
In addition, a navigation controller object automatically assigns itself as the delegate of its UINavigationBar object and prevents other objects from changing that relationship.
One/The? way to do what you want is to put in your own back-button.
In that Method you do your tests and call
[self.navigationController popViewControllerAnimated:true];
if the user is allowed to go back.

How to eliminate back bar button animation in a UISplitViewController app?

In a non-IUSplitViewController app, I am able to suppress the default back bar animation by adding this to my UIApplicationDelegate class header:
#interface MyNavigationBar : UINavigationBar { } #end
#interface MyNavigationController : UINavigationController { } #end
along with this in the corresponding .m:
#implementation MyNavigationController
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
return( [super popViewControllerAnimated:NO] );
}
#end
#implementation MyNavigationBar
- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated
{
return( [super popNavigationItemAnimated:NO] );
}
#end
Of course I also assigned the Navigation Controller and Navigation Bar objects in MainWindow.xib to MyNavigationController and MyNavigationBar respectively in Interface Builder.
This works like a charm in a standard application.
My problem is achieving the same thing in a UISplitViewController app.
Specifically, I cannot figure out how to override the default behavior of UINavigationBar in that case in order to suppress animation of the navigation bar when a view controller is popped via the back bar button.
I can override the behavior of UINavigationController by doing this whenever I instantiate a UIViewController as the root of the UISplitViewController right pane:
[split is a pointer to my UISplitViewController]
MyNavigationController *nc = (MyNavigationController *) [split.viewControllers objectAtIndex:1];
nc = [[[MyNavigationController alloc] initWithRootViewController:someController] autorelease];
split.viewControllers = [NSArray arrayWithObjects: [split.viewControllers objectAtIndex:0], nc, nil];
split.delegate = someController;
To recap, when I hit the back bar button in my UISplitViewController app, the content area of the active view controller does not animate when popped via the back bar button, but the navigation bar does animate, which looks dopey.
I found the solution for the standard application case in this forum, but saw no mention of a UISplitViewController solution.
I tried overriding initWithCoder in MyNavigationController to assign an instance of MyNavigationBar to the navigationBar attribute, but it wouldn't let me since it is read-only.
Stumped.

Suppress moreNavigationController in custom UITabBarController

I have implemented a custom UITabBar solution for a project. Essentially, if there are more than 5 items, I use a scrollView that will allow the user to scroll through the additional tab items and suppresses the more button. A similar look and feel can be seen in the Weather Channel app.
Each tab bar item corresponds to a UINavigationController that manages the stack of views for each tab. The issue I'm having is when I have more than 5 tab items, from tab 5 onward does not correctly maintain the navigation stack. It seems that the moreNavigationController kills the navigation stack each time you return to that tab and you are brought to the initial page again.
I've overridden the setSelectedViewController method as follows:
- (void) setSelectedViewController:(UIViewController *)selectedViewController {
[super setSelectedViewController:selectedViewController];
if ([self.moreNavigationController.viewControllers count] > 1) {
self.moreNavigationController.viewControllers = [[NSArray alloc] initWithObjects:self.moreNavigationController.visibleViewController, nil];
}
}
This code will remove the More functionality on the left nav button but it doesn't solve the issue of maintaining the navigation stack. All other tabs work fine. I can traverse down several views and the stack is maintained after I leave and return to that tab. I understand that this is a complicated issue so please let me know if there are areas where I can provide clarity. Thanks!
This is how I ended up fixing this:
- (void) setSelectedViewController:(UIViewController *) selectedViewController {
self.viewControllers = [NSArray arrayWithObject:selectedViewController];
[super setSelectedViewController:selectedViewController];
}
Basically any tab from 5 on gets its navigation controller replaced by the moreNavigationController when you intiially set the viewControllers on UITabBarController. Therefore, I dynamically set viewControllers to just contain the tab I'm clicking. There never ends up being more than 1 in this case so the moreNavigationController doesn't come into play.
When I init my custom controller, I just supply the first tab as the viewControllers so the application can load.
- (id) init {
self = [super init];
if (self) {
self.delegate = self;
[self populateTabs];
}
return self;
}
- (void) populateTabs {
NSArray *viewControllers = [self.manager createViewsForApplication];
self.viewControllers = [NSArray arrayWithObject:[viewControllers objectAtIndex:0]];
self.tabBar.hidden = YES;
MyScrollingTabBar *tabBar = [[MyScrollingTabBar alloc] initWithViews:viewControllers];
tabBar.delegate = self;
[self.view addSubview:tabBar];
}
For clarity, the tabBar delegate is set to this class so that it can respond to tab clicks. The delegate method is as follows:
- (void) tabBar:(id) bar clickedTab:(MyScrollingTabBarItem *) tab {
 if (self.selectedViewController == tab.associatedViewController) {
[(UINavigationController *) tab.associatedViewController popToRootViewControllerAnimated:YES];
} else {
self.selectedViewController = tab.associatedViewController;
}
// keep nav label consistent for tab
self.navigationController.title = tab.label.text;
}
You may also override the moreNavigationController var of UITabBarController and return your own custom NavigaitonContorller like so:
override var moreNavigationController: UINavigationController {
return MyCustomNavController()
}
worked in my case where I needed a NavigaitonController that allowed hiding the NavigationBar when scrolling.
As for hiding the Back Button of the MoreNavigationController you can set the leftBarButtonItem of each NavigationItem to anything (even an empty UIBarButtonItem) and the back butotn of the more NavigaitonController will disappear.

Prevent UINavigationBar popViewController animation

I have the following problem: I have overridden popViewControllerAnimated:(BOOL)animated of UINavigationController because I would like to have a custom animation. The code is as follows:
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
UIViewController *poppedCtrl = [super popViewControllerAnimated:NO];
[((customViewController *) self.topViewController) doCustomAnimation];
return poppedCtrl;
}
Unfortunately the UINavigationBar seems to ignore that I explicitly disable the built in animation and it is still animated.
What do I have to do to also prevent the animation of the navigation bar?
After some reading and also some experimentation I finally found out what needs to be done to achieve the desired behavior.
To prevent the navigation bar from being animated it is not sufficient to override (UIViewController *)popViewControllerAnimated:(BOOL)animated.
It is also necessary to create a custom navigation bar and override (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated:
- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated {
return [super popNavigationItemAnimated:NO];
}
Of course this custom navigation bar must also be the one which is used (I just replaced the navigation bar which is used by my navigation controller in the interface builder).
If anyones looking to disable push animation - this works for me, by overrideing this method on UINavigationBar:
- (void)pushNavigationItem:(UINavigationItem *)item {
NSMutableArray* items = [[self items] mutableCopy];
[items addObject:item];
self.items = items;
}

Resources