I was just wondering if there is a simple way to specify whether a UINavigationBar should overlay its content when shown. I currently have a UINavigationController that contains a custom UIViewController with a UIScrollView, which contains a UIPageViewController (I wanted a zooming/scrollable UIPageViewController).
When I call:
[self setNavigationBarHidden:NO animated:YES];
From within my UINavigationController, the UINavigationBar animates in, but pushes the custom container with all its content down, instead of overlaying it.
The bar is set to translucent and I've tried all the settings I can think of. I changed the extendEdges settings in the child view controllers and that resized the content when the navigation bar came in, instead of pushing it down. But I still can't work out how to get it to overlay instead.
Many thanks.
Ok, apologies. This just reveals how little I know about iOS programming...
I noticed that the container's view origin y value was -44. So adding the following within my container class:
- (void) viewDidLayoutSubviews
{
CGRect boundsRect = self.view.bounds;
boundsRect.origin = CGPointMake(0, 0);
self.view.bounds = boundsRect;
}
Results in the view staying at the top of the screen, so the UINavigationBar overlays it nicely when it appears.
EDIT: Actually the proper way appears to be just calling:
self.automaticallyAdjustsScrollViewInsets = NO;
Related
I'm having a problem with the prompt on a UINavigationItem that I just can't resolve...
I have a master and a detail view controller. When I push from the master to the detail a prompt is shown on the detail view controller:
However, when I pop back to the master view controller, the view isn't resized and the window shows through (the window has been coloured red):
This only happens on iOS7, on iOS6 the view resizes as expected.
I've tried a few things such as setting the prompt to nil in viewWillDisappear or viewDidDisappear but nothing seems to fix it.
If I set the navigation bar in the navigation controller to translucent it does fix this - unfortunately that's not an option.
I've created a very small example project here which demonstrates the issue: https://github.com/InsertWittyName/NavigationItemPrompt
Thanks in advance for any help!
A solution I can think of is to subclass the UIView of the master, and implement viewDidMoveToSuperview to set the frame of the view to be from the navigation bar's height to the end of the superview. Since the navigation bar is not translucent, your job is easier, as you don't have to take into account layout guides and content insets.
A few things to notice. When pushing and popping, the system moves your view controller's view into another superview for the animation and then returns it to the navigation controller's private view hierarchy. Also, when a view goes outside of the view hierarchy, the superview becomes nil.
Here is an example implementation:
#interface LNView : UIView
#end
#implementation LNView
- (void)viewDidMoveToSuperview
{
[super viewDidMoveToSuperview];
if(self.superview != nil)
{
CGRect rect = self.superview.bounds;
rect.origin.y += 44;
rect.size.height -= 44;
[self setFrame:rect];
}
}
#end
This is not a perfect implementation because it uses a hardcoded value for the navigation bar's height, does not take into account a possible toolbar, etc. But all these you can add as properties to this view and in viewDidLoad, before it starts going into the view hierarchy, set the parameters according to your needs.
You can remove the prompt when the user taps the back button, like this
override func willMove(toParentViewController parent: UIViewController?) {
super.willMove(toParentViewController: parent)
if parent == nil {
navigationItem.prompt = nil
}
}
The problem exists whether your nav bars are opaque or translucent. It sucks that Apple has allowed this heinous bug to plague us for over three years now.
All of these solutions are hacks. My solution is to either A) NEVER use prompts, or B) use them in every single view even if you have to set them to "".
You've given the answer yourself - brilliantly. It's a bug, but checking Translucent avoids the bug. Therefore the solution is to check Translucent and then compensate so that the nav bar looks the way you want.
To do so, make a small black rectangle image and include it in your project. Set the background image of the nav bar to this image. Check Translucent. Problem solved! The nav bar is now black opaque in appearance, but it no longer exhibits the bug.
Swift version:
class PromptViewSideEffect: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview: UIView = self.superview {
let rect: CGRect = superview.bounds
rect.origin.y += 44
rect.size.height -= 44
self.frame = rect
}
}
}
I have been reading a lot about iOS7 UI transition.
I am not able to get what these three properties automaticallyAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout??
For example I am trying to make my view controllers start below the status bar but I am not able to achieve it.
Starting in iOS7, the view controllers use full-screen layout by default. At the same time, you have more control over how it lays out its views, and that's done with those properties:
edgesForExtendedLayout
Basically, with this property you set which sides of your view can be extended to cover the whole screen. Imagine that you push a UIViewController into a UINavigationController. When the view of that view controller is laid out, it will start where the navigation bar ends, but this property will set which sides of the view (top, left, bottom, right) can be extended to fill the whole screen.
Let see it with an example:
UIViewController *viewController = [[UIViewController alloc] init];
viewController.view.backgroundColor = [UIColor redColor];
UINavigationController *mainNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
Here you are not setting the value of edgesForExtendedLayout, therefore the default value is taken (UIRectEdgeAll), so the view extends its layout to fill the whole screen.
This is the result:
As you can see, the red background extends behind the navigation bar and the status bar.
Now, you are going to set that value to UIRectEdgeNone, so you are telling the view controller to not extend the view to cover the screen:
UIViewController *viewController = [[UIViewController alloc] init];
viewController.view.backgroundColor = [UIColor redColor];
viewController.edgesForExtendedLayout = UIRectEdgeNone;
UINavigationController *mainNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
And the result:
automaticallyAdjustsScrollViewInsets
This property is used when your view is a UIScrollView or similar, like a UITableView. You want your table to start where the navigation bar ends, because you wont see the whole content if not, but at the same time you want your table to cover the whole screen when scrolling. In that case, setting edgesForExtendedLayout to None won't work because your table will start scrolling where the navigation bar ends and it wont go behind it.
Here is where this property comes in handy, if you let the view controller automatically adjust the insets (setting this property to YES, also the default value) it will add insets to the top of the table, so the table will start where the navigation bar ends, but the scroll will cover the whole screen.
This is when is set to NO:
And YES (by default):
In both cases, the table scrolls behind the navigation bar, but in the second case (YES), it will start from below the navigation bar.
extendedLayoutIncludesOpaqueBars
This value is just an addition to the previous ones. By default, this parameter is set to NO. If the status bar is opaque, the views won't be extended to include the status bar, even if you extend your view to cover it (edgesForExtendedLayout to UIRectEdgeAll).
If you set the value to YES, this will allow the view to go underneath the status bar again.
If something is not clear, write a comment and I'll answer it.
How does iOS know what UIScrollView to use?
iOS grabs the first subview in your ViewController's view, the one at index 0, and if it's a subclass of UIScrollView then applies the explained properties to it.
Of course, this means that UITableViewController works by default (since the UITableView is the first view).
Not sure if you are using storyboards, but if you are, to make your view controllers start below the status bar (and above the bottom bar):
Select the view controller in IB,
In the attributes inspector, deselect 'Extend Edges - Under Top Bars' and 'Extend Edges - Under Bottom Bars'.
I am using storyboards and using the above advice worked however I wasn't exactly sure how to implement it. Below is a short example in swift of how it cleared up the problem by putting the recommended solution into the ViewController.
import Foundation
import UIKit
// This ViewController is connected to a view on a storyboard that
// has a scrolling sub view.
class TheViewController: UIViewController {
// Prepares the view prior to loading. Putting it in viewDidAppear didn't work.
override func viewWillAppear(animated: Bool) {
// this method is an extension of the UIViewController
// so using self works as you might expect.
self.automaticallyAdjustsScrollViewInsets = false
// Default is "true" so this sets it to false tells it to use
// the storyboard as you have it placed
// and not how it thinks it should place it.
}
}
My Problem:
Auto Adjust set to true by default causing a difference between storyboard design and simulator
Resolved:
Code above applied, turning off the auto-adjust.
I solved this problem by adding this line, but my problem was related to a UIView, not UIScrollView
self.navigationController.navigationBar.translucent = NO;
Just bare in mind that
automaticallyAdjustsScrollViewInsets
property works only if some kind of scroll view (table view, collection view,...) is either
The view of VC, or
First subview of this view
Other suggested, that it doest work even if it is the first subview, but there are other scroll views in the view hierarchy.
EDIT (extension DIY)
If you want similar behaviour even if you can't fulfil these conditions (e.g. you have a background image below the scroll view), you can adjust the scroll view insets manually. But please don't set it to constant like 44 or 64 or even 20 like many suggest around SO. You can't know the size ever. There can be the incall/gps/audio notification, navigation bar doesn't have to be always 44 pts etc.
I think the best solution is to use layoutGuide length in didLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentInset = UIEdgeInsets(top: topLayoutGuide.length, left: 0, bottom: 0, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
You can use the bottomLayoutGuide in the same way.
In storyboard, in a view controller I tried add a navigation bar under the status bar, running it, it is transparent and shows a label that's supposed to be blurred, like by navigation bar.
But when placing the same view controller embedded in a navigation view controller, the underneath background image could be blurred, which is my intention.
What are these two way different results? What need to do for the firs method to make status bar blur?
Thanks!
In iOS 7 the status bar is transparent by default. The blurring you're seeing when there's also a navigation bar is actually created by the navigation bar. So to create the effect you're looking for without a navigation bar, you need to position a view that produces a blurring effect beneath the status bar.
For reference, add your view with a frame provided by:
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
I know this is old, just for reference, I solved this by setting self.navigationController.navigationBar.clipToBounds = NO
I haven't tested this completely, but go to your plist file and check the following settings:
"View controller-based status bar appearance": If this is set to "Yes", then it should display a status bar that is unique to each View Controller, which might be what you need.
"Status bar style": You may set this to three different styles: Opaque black, Gray, and Transparent black.
Let me know if this worked for you.
UINavigationController will alter the height of its UINavigationBar to either 44 points or 64 points, depending on a rather strange and undocumented set of constraints. If the UINavigationController detects that the top of its view’s frame is visually contiguous with its UIWindow’s top, then it draws its navigation bar with a height of 64 points. If its view’s top is not contiguous with the UIWindow’s top (even if off by only one point), then it draws its navigation bar in the “traditional” way with a height of 44 points. This logic is performed by UINavigationController even if it is several children down inside the view controller hierarchy of your application. There is no way to prevent this behavior.
It looks like you are positioning your view hierarchy in the first example starting at the point (0,20). Also, is that a UIToolbar or a UINavigationBar? If it's the latter, why are you using it by itself and not using it inside of UINavigationController?
If you do not use UINavigationController and are instead using custom view controller containers, you'll need to position your views accordingly.
See this answer for a thorough explanation.
I have similar UI design and based on Matt Hall answer and some article I've googled, I come up with something like this:
- (void)viewDidLoad {
[super viewDidLoad];
if (NSFoundationVersionNumber>NSFoundationVersionNumber_iOS_6_1) {
CGRect statusBarFrame = [self.view convertRect: [UIApplication sharedApplication].statusBarFrame fromView: nil];
UIToolbar *statusBarBackground = [[UIToolbar alloc] initWithFrame: statusBarFrame];
statusBarBackground.barStyle = self.navBar.barStyle;
statusBarBackground.translucent = self.navBar.translucent;
statusBarBackground.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
[self.view addSubview: statusBarBackground];
}
}
Where self.navBar points to navigation bar added in storyboard. This is needed only in case when it runs on iOS7 that is why I've added this condition (my app has to support iOS5).
This works like a charm.
alternative approach (enforce status bar size) is also good:
- (void)viewDidLoad {
[super viewDidLoad];
if (NSFoundationVersionNumber>NSFoundationVersionNumber_iOS_6_1) {
CGRect statusBarFrame = [self.view convertRect: [UIApplication sharedApplication].statusBarFrame fromView: nil];
self.navBar.frame = CGRectUnion(statusBarFrame, self.navBar.frame);
}
}
I've found another solution I think this is best since it involve only storyboard and no code is required.
Switch storyboard view to 6.1 mode (view as: iOS 6.1 and Earlier)
Select problematic UINavigationBar
in size section add 20 delta height in "iOS6/7 Deltas"
Switch back view to 7.0 mode (view as: iOS 7.0 and Later), and be happy with result.
when you embed view controller with navigation view controller that time you will see navigation bar to all the view controller you are pushing to from same view controller. In your first case you are adding the navigation bar object, insted of that you can select view controller from storyboard , go to attributes inspector tab & from their select Top bar as translucent navigation bar.
I have an app where up until now I've been using a UINavigationController with a UINavigationBar that has its property translucent = YES. This means the UINavigationController's content view (i.e. the views from the view controllers you push) to be full-screen (minus status bar).
However, if you set the navigationBar.translucent = NO, this container view becomes 44pt shorter, as I suppose Apple has assumed you don't need any content under an opaque navigationBar.
... except if you're doing what we're doing and are employing a navigationBar that scrolls away (see This Post on how to do that) So I'd like to know if this is possible.
I want to have translucent = NO, but have everything behave as if it were still set to YES. I like the functionality of the translucent = YES, but I don't actually want the bar to be made translucent by UIKit.
What worked for me was to add
extendedLayoutIncludesOpaqueBars = true
in
viewDidLoad
something like this
override func viewDidLoad() {
super.viewDidLoad()
extendedLayoutIncludesOpaqueBars = true
}
Hope it will work for you as well
It's not necessarily a good answer but you could just offset your view that high if you're not translucent.
//This won't take into account orientation and probably other details
if(!self.navigationController.navigationBar.isTranslucent)
{
self.view.frame = CGRectMake(0,0,-44,self.view.bounds.size.height);
}
You could put that in your viewDidLoad or viewWillAppear and if you have a bunch of view controllers you can just subclass them all and put your logic in the subclass.
I found a solution that works, although it is indeed a bit of a hack.
The idea is to give the translucent nav bar an opaque backing. Unfortunately I'm not happy with the solution in that it's dirty and not encapsulated and introduces some potential issues, but i AM happy because it got the job done.
In my Application's base view controller class (i.e. MyViewController : UIViewController), in the viewDidLoad method, I instantiate a new ivar UIView *_navigationBarBG and give it the same frame as self.navigationController.navigationBar. I then set it's backgroundColor property to [UIColor whiteColor] although this is how you achieve some more tint I guess. [EDIT:If you wanted to be a purist (color values remaining exactly as they come from the .psd), you could make the _navigationBarBG a UIImageView and use your custom background there, and the background of the actual UINavigationBar you set to draw clear (or stretch a 1px transparent image if you wanted to use a typical 'change your navigation bar using an image' recipe that's somewhere on the internet)]
if(self.navigationController)
{
_navigationBarBG = [[UIView alloc] initWithFrame: self.navigationController.navigationBar.frame];
_navigationBarBG.backgroundColor = [UIColor whiteColor];
[self.view addSubview:_navigationBarBG];
}
THEN, (and this is the crappy part, but I don't see any other way), I add this view as a subview. BUT, whenever you would normally make a call to [self.view addSubview: anyView], you have to make sure you call [self.view insertSubview: anyView belowSubview: _navigationBarBG];
if (_navigationBarBG)
[self.view insertSubview: anyView belowSubview:_navigationBarBG];
else
[self.view addSubview: anyView];
If you forget that, these added views will slide under your navbar background and look weird. So you need to know that this is a source of error.
WHY AM I DOING THIS? Again you might ask... I want to be able to have a scrolling navigation bar that scrolls out of the way when you scroll down your table view, thereby giving the user more screen space. This is done by using the scrollView delegate (scrollViewDidScroll:) and also viewWillAppear:
// FIRST DEAL WITH SCROLLING NAVIGATION BAR
CALayer *layer = self.navigationController.navigationBar.layer;
CGFloat contentOffsetY = scrollView.contentOffset.y;
CGPoint newPosition;
if (contentOffsetY > _scrollViewContentOffsetYThreshold && self.scrollingNavigationBarEnabled) {
newPosition = CGPointMake(layer.position.x,
22 - MIN((contentOffsetY - _scrollViewContentOffsetYThreshold), 48.0)); // my nav bar BG image is 48.0 tall
layer.position = newPosition;
[_navigationBarBG setCenter: newPosition]; // if it's nil, nothing happens
}
else
{
newPosition = kNavBarDefaultPosition; // i.e. CGPointMake(160, 22) -- portrait only
layer.position = newPosition;
[_navigationBarBG setCenter: newPosition]; // if it's nil, nothing happens
}
I was looking for an answer to this as I wanted my subviews to be at (0,0) and not (0,44)(in reference to the Screen bounds), but I could not find an answer on how to set this in the NavigationController, which I thought would be an included property.
What I ended up doing that was very simple is adding a subview to the navigation controller that was the width and height of the Navigation Bar, but then insert the subview below the Navigation Bar.
Now the setting is Translucent = YES, but it still appears solid and the subviews behave how I want.
EDIT: After re-reading your original post, I suppose if you're going to be rolling the nav bar away, you'll have to take into account hiding and showing the new subview as you do the same with the nav bar
I want to add a image just above the navigation bar. Here is the final result picture I want:
click me
At the beginning, I think it is quite simple:
Using UIBuilder add one UIImage and one UIView
Add navigation bar controller to UIView as its root view
The Hierarchy I thought should like this: UIViewController->UIView->NavigationBarController.(Here the UIView is one subview of the view of UIViewController)
Below is one of the code I tried, subView is the IBOutlet of one UIView builed by UIBuilder
UINavigationController *test;
test=[[UINavigationController alloc]init];
[[subView window] setRootViewController:test];
[subView.window makeKeyAndVisible];
But after trying several times,I found it is not working.
Does anyone do the same work before? If so, please give me some suggestions.
self.navigationController.navigationBar.frame = CGRectMake(0, //height of imageView//, self.view.bounds.size.width, 44.0f);
CodaFi's suggestion is almost there.
Try this:
test.view.frame = CGRectMake(0, //height of imageView//, self.window.bounds.size.width, //(total height of content... e.g. 460.0f if you leave the status bar visible)-(height of imageView)//);
There is one thing to note though... The navigation controller likes to take up all the usable space on screen so sometimes it will automatically resize its view to a rect like this, {{0.0f,0.0f},{320.0f,460.0f}} after rotating the device. I have experienced this many times on the iPad. You might have to start listening for the rotation event, and reset the frame of the navigation controller's view on every rotation to one that doesn't block your image.