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.
Related
I am using storyboards to build my app's UI. Essentially, I am opening a UINavigationController as modal view, and in this navigation controller, I embed as rootViewController an instance of another UIViewController (Location Selection View). This is all set up in storyboard and looks basically like this:
Now, I want to access the navigation controller in the viewDidLoad of LocationSelectionViewController in order to include a UISearchBar in the navigation bar with:
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;, this doesn't work however, because my UINavigationController is nil at this point, I know because I set a breakpoint and logged it:
(lldb) po self.navigationController
nil
Does anyone know why or what I have to do so that there is actually an instance of UINavigationController accessible on my LocationSelectionViewController?
UPDATE: Here is more code, the header really only consists of the declarations
LocationSelectionViewController.h
#protocol LocationSelectionViewControllerDelegate <NSObject>
- (void)setLocation:(Location *)location;
#end
#interface LocationSelectionViewController : UIViewController <GMSGeocodingServiceDelegate, UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, GMSMapViewDelegate>
#end
Parts of LocationSelectionViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar.text = DUMMY_ADDRESS;
self.previouslySearchedLocations = [[CoreDataManager coreDataManagerSharedInstance] previouslySearchedLocations];
self.searchResults = [[NSMutableArray alloc] initWithArray:self.previouslySearchedLocations];
self.mapView.delegate = self;
self.gmsGeocodingService = [[GMSGeocodingService alloc] initWithDelegate:self];
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self addMapView];
}
OK, I just solved my problem. I strongly believe it was a bug in interface builder. I deleted the old navigation controller and just dragged and dropped a new one onto the storyboard, now calling po self.navigationController in viewDidLoad actually returns an instance of UINavigationController. Thanks for all the help though, I appreciate it a lot!
I Have UINavigationController in my app, I want to have a right UIBarButtonItem to be shown in all navigation bar that appear in my application. this button will load menu, so I don't want to add this button in every navigation bar manually, also as the function is loading menu, I don't want to copy/past action for this button.
is there any way to handle this in ViewController.h and .m ?
so the button act as a universal bar button item?
What you can do is subclass the navigation controller. Here is an example
#interface NavigationController : UINavigationController
#end
#interface NavigationController () <UINavigationBarDelegate>
#end
#implementation NavigationController
- (void)viewDidLoad
{
[super viewDidLoad];
for (UIViewController* viewController in self.viewControllers){
// You need to do this because the push is not called if you created this controller as part of the storyboard
[self addButton:viewController.navigationItem];
}
}
-(void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
[self addButton:viewController.navigationItem];
[super pushViewController:viewController animated:animated];
}
-(void) addButton:(UINavigationItem *)item{
if (item.rightBarButtonItem == nil){
item.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:#selector(action:)];
}
}
-(void) action:(UIBarButtonItem*) button{
}
#end
This cannot be done unless you use a custom view to look like NavigationBar.
By default, NavigationController clears all bar button items when a ViewController is pushed or popped. So for every ViewController, you need to create UIBarButtonItem every time in function
- (void)viewWillAppear:(BOOL)animated
or use can subclass UINavigationController and do as #rp90 answer.
You can add a button that appears in your UINavigationController by adding it in the UINavigationController's delegate - in this example, a singleton helper class.
Swift 3:
class NavHelper: UINavigationControllerDelegate {
static let shared = NavHelper()
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let barButtonItem = UIBarButtonItem(image: <yourButtonImage>, style: .plain, target: NavHelper.shared, action: #selector(NavDelegate.handleButton(_:)))
viewController.navigationItem.rightBarButtonItem = barButtonItem
}
func handleButton(_ sender: UIBarButtomItem) {
<yourCode>
}
}
Another option is to make your own navigation bar view as a part of the UIViewController. You can turn off Apple's and build your own. We did this to provide our own controls easier. The only thing you lose is the easy translucency under iOS 7. A lot of apps do this for the same reason we did.
You can create a new class that inherits from UIViewController and in the viewDidLoad method, create a UIBarButtonItem and add it to the navigationItem as a left/right bar item. Let's say this class is called CustomBarViewController:
UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithTitle:#"hi" style:UIBarButtonItemStylePlain target:self action:#selector(doSomething:)];
[self.navigationItem setRightBarButtonItem:barItem];
In your existing view controllers, instead of having them inherit from UIViewController in the .h file, you can have them use CustomBarViewController instead:
#interface MyExistingViewController : CustomBarViewController
You can then put actions into the doSomething: method, or have it pass notifications to your existing view controllers.
You can add the button into a ContainerView. Thus it will be on at all times.
Food for thought....
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).
The iOS 7 Transition Guide give a good hint how to change the UIStatusBarStyle dynamically in a UIViewController using
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleDefault;
}
together with [self setNeedsStatusBarAppearanceUpdate];
This works fine in a single view application. However, I'm now trying to change the UIStatusBarStyle in a modal view to UIStatusBarStyleLightContent. There is a MainViewController which segues to the ModalViewController, which itself is embedded in a NavigationController. The ModalViewController has set its delegate to the MainViewController.
I tried to call [self setNeedsStatusBarAppearanceUpdate]; in the ModalViewController together with the following method in that class without effect:
// In ModalViewController.m
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
I also tried to call [self setNeedsStatusBarAppearanceUpdate]; in MainViewController on prepareForSegue: sender: method with conditions in - (UIStatusBarStyle)preferredStatusBarStyle {} to return UIStatusBarStyleLightContent when the modal view is presented - but that has no effects, too.
How can I change the UIStatusBarStyle in the modal view?
EDIT: Post updated: I need to mention that the ModalViewController is embedded in a NavigationController with a NavigationBar. With NavigationBar set to hidden to above call of [self setNeedsStatusBarAppearanceUpdate]; in ModalViewController works fine. But not when the Bar is visible.
You need a ViewController that's showing in Fullscreen to return the appropriate status bar infos. In your case: The NavigationController which contains ModalViewController needs to implement preferredStatusBarStyle and return UIStatusBarStyleLightContent.
A call to setNeedsStatusBarAppearanceUpdate is only necessary if the values a view controller returns actually change. When the view controller is first presented they are queried anyway.
We should notice that non-fullscreen modalVC CAN use modalPresentationCapturesStatusBarAppearance to control the statusBar style.
Anyone who wanna know more about Status Bar control should not ignore the UIViewController Managing the Status Bar.
Update at 2015-11-06:
And make sure you have set UIViewControllerBasedStatusBarAppearance described in iOS Keys
Update at 2018.04.09:
I noticed that viewController in a navController may not get call prefersStatusBarHidden with iOS 10.0 - 10.2. Custom your navigationController to ensure that
#implementation YourCustomNavController
//for iOS 10.0 - iOS 10.2
- (BOOL)prefersStatusBarHidden {
UIViewController *childVC = [self childViewControllerForStatusBarHidden];
if (childVC) {
return [childVC prefersStatusBarHidden];
}
return [super prefersStatusBarHidden];
}
#end
And anyone who want to go deeper inside can dig into UIKit +[UIViewController _currentStatusBarStyleViewController] using Hopper or IDA Pro. It may helps you solve these kinds of bugs.
The key to making this work is that only the fullscreen view controller get's to dictate the style of the status bar.
If you are using a navigation controller and want to control the status bar on a per view controller basis, you'll want to subclass UINavigationController and implement preferredStatusBarStyle such that it returns the topViewController's preference.
Make sure you change the class reference in your storyboard scene fromUINavigationController to your subclass (e.g. MyNavigationController in the example below).
(The following works for me. If your app is TabBar based, you'll want to do something similar by subclassing the UITabBarController but I haven't tried that out).
#interface MyNavigationController : UINavigationController
#end
#implementation MyNavigationController
- (UIStatusBarStyle)preferredStatusBarStyle
{
return self.topViewController.preferredStatusBarStyle;
}
#end
To change the status bar of the UINavigationController embedding your ViewController without subclassing UINavigationController, use this:
navigationController?.navigationBar.barStyle = .Black // to make the status bar text white
.Black will make the text white (status bar and the view's title), while .Default has a black title and status bar.
I had a side menu/reveal controller (SWRevealController) which turns out to always be the root controller for status bar queries. Overriding childViewControllerForStatusBarStyle let me re-route the query to the front most controller.
/**
This view is always considered the topmost for status bar color queries.
Pass the query along to what we're showing in front.
*/
- (UIViewController *)childViewControllerForStatusBarStyle
{
UIViewController *front = self.frontViewController;
if ([front isKindOfClass:[UINavigationController class]])
return ((UINavigationController*)front).topViewController;
else
return front;
}
It seems like the app goes off the statusBarStyle of the topmost viewController. So if you add another viewController on top of your current one, it now gets its cues from the new viewController.
This works for me:
Set View controller-based status bar appearance to NO
Set Status bar style to UIStatusBarStyleLightContent (just copy that value)
In appDelegate use [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
Hope it helps (ref: ios7 status bar changing back to black on modal views?)
Just look up if your app's rootViewController need to override -(UIStatusBarStyle)preferredStatusBarStyle method
All of the above work. However sometimes I find it really a pain in the bottom to go and change every instance in the Storyboard etc... So here's something that works for me that also involves subclassing.
First create the subclass:
#interface HHNavLightColorBarController : UINavigationController
#end
#implementation HHNavLightColorBarController
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
#end
Then using the magic of Objective-C and a little bit of the <objc/runtime.h>
When you have a reference of the view controller and your presenting it:
UINavigationController *navVC = ...; // Init in your usual way
object_setClass(navVC, [HHNavLightColorBarController class]);
[self presentViewController:nav animated:YES completion:^{
NSLog(#"Launch Modal View Controller");
}];
Sometimes it seems a bit less intrusive. You could probably even create a category that checks to see if your kindOfClass is a navigation controller and auto do it for you. Anyways, the answer is above by jaetzold, I just found this to be handy.
I have an application with UITabBarController as its main controller.
When user taps a button(not in the tab bar, just some other button), I want to add new UIViewController inside my UITabBarController and show it, but I don't want for new UITabBarItem to appear in tab bar. How to achieve such behaviour?
I've tried to set tabBarController.selectedViewController property to a view controller that is not in tabBarController.viewControllers array, but nothing happens. And if I add view controller to tabBarController.viewControllers array new item automatically appears in the tab bar.
Update
Thanks to Levi, I've extended my tab bar controller to handle controllers that not present in .viewControllers.
#interface MainTabBarController : UITabBarController
/**
* By setting this property, tab bar controller will display
* given controller as it was added to the viewControllers and activated
* but icon will not appear in the tab bar.
*/
#property (strong, nonatomic) UIViewController *foreignController;
#end
#import "MainTabBarController.h"
#implementation MainTabBarController
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
self.foreignController = nil;
}
- (void)setForeignController:(UIViewController *)foreignController
{
if (foreignController) {
CGFloat reducedHeight = foreignController.view.frame.size.height - self.tabBar.frame.size.height;
foreignController.view.frame = CGRectMake(0.0f, 0.0f, 320.0f, reducedHeight);
[self addChildViewController:foreignController];
[self.view addSubview:foreignController.view];
} else {
[_foreignController.view removeFromSuperview];
[_foreignController removeFromParentViewController];
}
_foreignController = foreignController;
}
#end
The code will correctly set "foreign" controller's view size and remove it when user choose item in the tab bar.
You either push it (if you have a navigation controller) or add it's view to your visible View Controller's view and add it as child View Controller also.
You can either present the new view controller with:
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion;
Or, if one of your UIViewControllers is inside a UINavigationController, you can:
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;