Adjusting views when Status Bar hides on rotation - ios

I've browsed around looking for an answer for this, but I've only found people with similar problems and not this exact problem, so hopefully someone here can help me!
I have an iPad App. On iPad, when you hold the iPad in either portrait or landscape, the status bar with the clock and battery is always shown. For this reason, I have some custom toolbars at the top of some of my View Controllers that account for those 20 points.
I am now working on making my App universal. The first thing I noticed is that when the iPhone is help in portrait mode the status bar is shown, but when it's held in landscape mode, the status bar hides and now I have a toolbar that is 20 points too tall.
I am not interested in either always hiding or always showing the status bar. I think the functionality of hiding it on landscape mode on the phone is fine. What I want to do is to be able to detect when the status bar is hidden so that I can adjust the Y position of my toolbar (so make it either 0 or -20). An even better solution would be if there's a way for me to set my layout constraints to handle this scenario.

The correct approach is:
Use a normal toolbar with a normal height - do not set any constraint on the height.
Add leading constraint 0 to superview (not superview margin), trailing constraint 0 to superview (not superview margin).
Add top constraint 0 to top layout guide (or top of safe area). This will appear to leave 20 pixels of space above the toolbar, but laugh an evil laugh and proceed.
Set the view controller as the toolbar's delegate (there is a delegate outlet for this purpose).
Have the view controller adopt UIBarPositioningDelegate and implement the delegate method as follows:
class ViewController: UIViewController, UIBarPositioningDelegate {
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}
This will cause the apparent height of the toolbar to be extended up behind the status bar when there is a status bar, but it will have normal height and be smack against the top when there is no status bar.

Related

Exclude navigation bar from iOS "safe area"

In my iOS app, I have views rendered beneath the navigation bar. This is because the navigation bar is hidden until the user taps the screen.
The screenshots below illustrate my problem.
The "X" button is rendered beneath the iPhone X's notch and can hardly be seen. This is when I constrain the button's topAnchor to its superview!.topAnchor.
Note that this works as intended for all devices except the iPhone X.
The "X" button in this screenshot is anchored to its superview!.safeAreaLayoutGuide.topAnchor and renders below the navigation bar. This makes sense given Apple's documentation on safeAreaLayoutGuide:
"this guide reflects the portion of the view that is not covered by navigation bars, tab bars, toolbars, and other ancestor views."
However, I want the "X" button to render below the iPhone X's notch and underneath the navigation bar. Here's what it looks like when the navigation bar is hidden:
The "X" button should render right below the notch.
So my question is:
Is there a way to exclude the navigation bar from a view's safeAreaLayoutGuide? And if not, what are my options, other than manually offsetting the button on iPhone X's.
Note that I'm doing everything programmatically. I no longer use Storyboards.
Thanks!
You can change the view controllers safe area insets by adding additional insets.
Create a UIEdgeInsetsMake() object where you subtract the navigation bar height from the top inset. Then add it to the View Controllers additionalSafeAreaInsets.
Declaration: var additionalSafeAreaInsets: UIEdgeInsets { get set }
This is not an answer, but a workaround:
Keep a reference to the "X" button's top constraint.
In layoutSubviews(), update the constant of the top constraint depending on the "X"'s superview and window safeAreaInsets.
override func layoutSubviews() {
// To support devices with a "safe area". If the receiver has a top safe area, position
// the close button beneath the _window's_ safe area.
// Note that using the receiver's safe area does not work in this case because it includes
// the navigation bar. We want to render the close button beneath the navigation bar.
let windowSafeAreaInsets = UIApplication.shared.keyWindow!.safeAreaInsets
// Only use the safe area if the receiver _and_ window have a top safe area. This handles
// the case of non-safe area devices using a hidden navigation bar.
closeButtonTopConstraint.constant = safeAreaInsets.top > 0 && windowSafeAreaInsets != .zero
? windowSafeAreaInsets.top : 16
// Possibly do something similar for landscape and the "X" button's trailing constraint.
}

Applying bar metrics on a standalone UINavigationBar

My app has a view controller that due to the fact it plays its own custom transition animations, provides its own standalone UINavigationBar view at the top (As opposed to using a UINavigationController).
When using an iPhone, and when rotating the device, I would like the UINavigationBar to automatically apply the landscape UIBarMetrics properties (eg, change height, change the background image, resize the buttons etc), but by default, it does not. This is a problem on iOS 7, since even if I manually change the height of the UINavigationBar, the UIBarButtonItem elements don't change their vertical positions.
Is there a way to manually 'tell' the UINavigationBar to apply specific bar metric properties to itself? Or is that actually an implementation inside UINavigationController, and not UINavigationBar?
After various testing and trial and error after asking this question, I eventually worked out a solution that fixed all of my issues, so I'll post it here under the solution I'd previously accepted.
When my app is displayed in landscape on an iPhone, I wanted the UINavigationBar at the top to shrink to the standardly accepted 32 points high, as is the case with any apps that use the UINavigationController class. However, as I am not using a UINavigationController for this particular view (for varying reasons of feasibility), I needed to implement this manually.
To account for the new transparent status bar in iOS 7, I adjusted the origin and size of the UINavigationBar so it encompassed both the bounds of the status bar, and the normal UINavigationBar region (ie, so the UINavigationBar frame origin was {0,0}, and the height was 52 points.)
Unfortunately, this happened:
While the bar itself is rendering at the proper position and height, all of the content in the bar, including the title and buttons are not positioned properly, being much too high, almost touching the status bar content.
It was pretty obvious what was happening. The navigation bar content is being vertically aligned to its own middle, completely disregarding the presence of the status bar content.
When I tested the same orientations with a normal UINavigationController, this was not the case, and the title and buttons in the UINavigationBar from the UINavigationController worked absolutely fine. Apple had done SOMETHING in there that wasn't part of the normal UINavigationBar implementation.
Going on this, I picked apart the view layout hierarchy of a UINavigationControllerto see what was happening to the UINavigationBar in there (Mainly calling a lot of NSLog() statements that would dump the subviews of the navigation bar.)
This is what I discovered:
From the looks of it, Apple have employed a relatively sneaky hack to achieve this effect. It turns out the actual UINavigationBar is actually placed right below the status bar (ie at point {0,20}) and only has a height of 32 points. Then, what happens is a private subview inside the UINavigationBar in charge of rendering the background is extended upwards, outside of the bounds of the navigation bar to encompass the region behind the status bar (ie, its origin is {0, -20}, and its height is 52 points, local to the navigation bar's subview coordinate space).
So by doing that, not only does the content vertically align properly, but the translucent effect still extends behind the status bar.
Anyway, after I discovered this, it was pretty straightforward to write a solution. All I needed to do was reposition and resize the UINavigationBar back to how I had it iOS 6 (ie, 20 points down, and only 32 points high), and then implement a UINavigationBar subclass that override the layoutSubviews method, grabbed the internal background view (Doing a quick subview check for a view with a class name that matched "Background"), and then manually extended it.
The bar metrics properties you can set on a UINavigation bar are things like background image and the title vertical position. Heigh and width need to be set from within your view controller.
If you need to manually tell the navigation bar to change it's size when the orientation changes you can implement the method - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration1 in your view controller and change the size there.
Another option you can use is to use autolayout to specify that the width of your navigation bar is pinned to the left and right sides of its superview and let it figure out how wide it should be. For example
UINavigationBar *bar = [[UINavigationBar alloc] init];
bar.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:bar];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[bar]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(bar)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[bar(44)]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(bar)]];

How do I make a constraint to position my label from the top, but not have it move when I hide the navigation bar (as it adjusts height of view)?

Long story short, I want to position my UILabel so that it's, say, 150pt from the top of the screen. This allows it to be a good distance from the top on both a 4" iOS device or a 3.5" iOS device.
However, as soon as I use [self.navigationController setNavigationBarHidden:YES animated:NO]; the navigation bar hides which causes the view to resize and get taller. This causes my UILabel to jump upward, which I don't want it to do. It's when the user wants to go into a full screen view, and in this case things shouldn't be jumping around.
Now you may say, make the constraint from the bottom of the view. That works in theory, but it causes the UILabel to be too far down the screen on an iPhone 5. I could hardcode the value, but that doesn't quite seem to be in the spirit of Auto Layout.
Basically, a perfect solution would be to be able to set the constraint to the top of the screen not the view. Is something like this possible? If not, how would I best do this?
Make an IBOutlet to the top constraint (I call it topCon in my example) and either add or subtract the height of the bar from the constraint's constant when you hide or unhide it.
- (IBAction)hideNavBar:(UIButton *)sender {
self.navigationController.navigationBarHidden = !self.navigationController.navigationBarHidden;
if (self.navigationController.navigationBarHidden) {
self.topCon.constant = self.topCon.constant + self.navigationController.navigationBar.frame.size.height;
}else{
self.topCon.constant = self.topCon.constant - self.navigationController.navigationBar.frame.size.height;
}
}
This will work correctly in both portrait and landscape modes where the navigation bar has different heights.
Why not use the navigation bar height (self.navigationController.navigationBar.frame.size.height), and move the UILabel up/down depending on whether the navigation bar is hidden / visible?
This way, you're not hardcoding anything and you're working off of the UINavigationBar properties.
AutoLayout deals with Views not screens unfortunately.
Here are some solutions:
1: You could say, reposition it yourself whenever layoutsubviews (or a similar trigger is performed).
2: You could also set the height to be variable so that it takes up slack when available.
3: Add a view that anchors from the bottom and fills up to the top (you can adjust the view to stay the same size or change, and add this label with a constrain to be at the top of this view).

Scroll bars with a translucent navigation bar

If you setTranslucent to YES on a UIViewController, it shifts the content 'up' (I suppose what it is really doing is not shifting it down).
Since all of my content is in a UIScrollView, the navigation bar is effectively covering the top of this scroll view. I can bump the items in the scroll view down a little, but the scroll indicators still get partially hidden under the nav bar.
I would consider moving the whole scroll bar down 44 points, except that's not going to look right in landscape orientation on an iPhone.
A vanilla UITableView seems to handle this scenario just fine - how can I do the same in my UIViewController?
EDIT
I've realized that I'm supposed to set contentInsets and scrollIndicatorInsets to 'shift' things down. But still my problem remains - In landscape mode, there is an ugly gap between the nav bar and the scroll view.
It took me 2 minutes to google this answer: contentInset
Check this question for an explanation: https://stackoverflow.com/a/10049782/2462469
After that you need to adjust ScrollIndicatorInsets as well.
http://developer.apple.com/library/ios/ipad/#documentation/UIKit/Reference/UIScrollView_Class/Reference/UIScrollView.html%23//apple_ref/occ/instp/UIScrollView/scrollIndicatorInsets
Edit: this fellow has similar rotation issues: https://stackoverflow.com/a/15623727/2462469
He needed to reset the scroll position after rotating back. It's an edge case though.

UIToolbarPosition set to bottom on resize

I have a UIToolBar at the top of one of my views and the shadowing shows up on the bottom of the tool bar as I'd expect. I know UIToolbarPosition is internally set to UIToolbarPositionTop.
When I rotate my device (iPad), the tool bar grows (using auto sizing in IB) and the UIToolbarPosition changes to UIToolbarPositionBottom which flips the shadow to be at the top of the bar. If I don't have the bar resize, it remains correct, but of course doesn't stretch to match screen width.
So, what gives? What would cause the UIToolbarPosition to flip on me? This is sitting at 0,0 the whole time and only changes width based on rotation.
Extra: I've considered work arounds like using a UINavigationBar. UIAppearance is probably a no go because I do have a toolbar at the bottom too and I do want my "top" different than my "bottom"
I eventually solved this using the UIBarPositioning protocol on UIToolBarand setting the barPosition = UIBarPositionTop

Resources