Determine NEW Frame of Navigation Bar BEFORE actually rotating - iOS - ios

I am using a a translucent Navigation Bar and Status Bar and my View Controller wants full screen. Thus, my View Controller's View extends under the Nav and Status bars and takes the full size of the screen.
I also have a Label which I would like to align directly under the Navigation Bar. Because I cannot add constraints directly between the Label and the Navigation Bar, I add my constraint between the Top of the Label and the Top of the it's Parent View. I set the constant of the contstraint to be equal to the height of the Status Bar + the height of the Navigation Bar.
The issue I have is during rotation between Portrait and Landscape, because the height of the Navigation Bar changes and I need my Label to rotate nicely as well, so I need to know the new height of the Navigation Bar in the willRotateToInterfaceOrientation: method.
I use this method to ensure the Label is in the correct location when the View Controller is navigated to from either portrait or landscape. It works fine.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Get Y origin for Label
CGFloat navBarY = self.navigationController.navigationBar.frame.origin.y;
CGFloat navBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat labelY = navBarY + navBarHeight;
// Set vertical space constraint for Label
self.labelConstraint.constant = labelY;
}
I use this method to reposition the Label when the orientation is changed, as the Navigation Bar height changes from 44px to 32px. Problem is, I need to get the NEW height that the navigation bar WILL BE after the rotation, BEFORE the rotation actually takes place.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Get Y origin for Label
CGFloat navBarY = self.navigationController.navigationBar.frame.origin.y;
CGFloat navBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat labelY = navBarY + navBarHeight;
// This obviously returns the the current Y origin for label before the rotation
// Which isn't very useful.
NSLog(#"labelY: %f", labelY);
// This code produces the desired effect, but obviously I want to avoid
// hard-coding the values.
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
self.labelConstraint.constant = 20 + 32;
} else {
self.labelConstraint.constant = 20 + 44;
}
}
For fun, I tried to set the Y origin for the Label after the rotation in didRotateFromInterfaceOrientation:, but as expected it's not smooth and the label snaps into place after the rotation is complete.
Thanks in advance for your assistance!

The answer is: Size Classes. You can inform yourself about it, it's well documented by Apple. You will have to use this function: func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator). When setting up your view I would suggest adding two properties in your view controller, both an array of NSLayoutConstraint, one for portrait mode, one for landscape. Since iOS 8, there is the possibility to activate/deactivate multiple constraints at once. In the above mentioned function, do the following:
if newCollection.verticalSizeClass == .Compact { //orientiation is landscape
NSLayoutConstraint.deactivateConstraints(self.portraitConstraints)
NSLayoutConstraint.activateConstraints(self.landscapeConstraints)
} else {
NSLayoutConstraint.deactivateConstraints(self.landscapeConstraints)
NSLayoutConstraint.activateConstraints(self.portraitConstraints)
}
Make sure to deactivate first, otherwise your constraints will conflict.
Sorry for the "swifty" answer, I suppose you'll be able to 'translate' it into ObjC. If you have any question, feel free to ask.

Related

UINavigationBar with Large Titles - how to find extra height in iOS 11

When using prefersLargeTitles for a UINavigationController's UINavigationBar in iOS 11, the nav bar increases height. The increase is from 44 to 96 on the iPhones I have checked, but I think those numbers can change per device (or at least we need to code as if they can).
I want to programmatically find the 'extra' height - the height of the large titles area that is added beneath the traditional UINavigationBar when a large title is displayed. I can easily find the entire height of the bar with the large title displayed, but is there a way to programmatically find the height of the large title portion of the bar alone (without any hardcoding)?
The reason I need this is that there are times that I want to programmatically scroll to the top of a UITableView, pulling down the large title (which has scrolled up under the "regular height" nav bar) so that it is showing, and the content offset I need is the extra height of the nav bar. I could use the total height of the navigation bar, but this would pull the UITableView down too far. To do this now, I need to hardcode as below:
[self.tableView setContentOffset:CGPointMake(0, -52) animated:NO];
I think you won't find a way to clearly resolve your problem. What you are asking for is a part of a navigation bar internals which is not exposed to the user. However, I think I can offer you a workaround.
To understand this let's take a look at the navigation bar (with large titles enabled) in the view debugger. As you can see on the below image there is an extra view containing the large title for a particular view controller. This view is not present when you don't support large titles.
Basically, what you want to know is the height of that view.
This is my proposed solution/workaround
extension UINavigationBar
{
var largeTitleHeight: CGFloat {
let maxSize = self.subviews
.filter { $0.frame.origin.y > 0 }
.max { $0.frame.origin.y < $1.frame.origin.y }
.map { $0.frame.size }
return maxSize?.height ?? 0
}
}
What it does
Filters out subviews which start on the top of the navigation bar. They are most probably a background or the navigation area, we don't need them.
Finds the lowest subview in the in the navigation bar.
Maps the resulted subview frame to the frame size.
Returns the height of the found subview or 0 if none was found.
The obvious drawback of this approach is that it's tightly coupled to the structure of the navigation bar which may change in the future. However, it's unlikely that you will get something more than one or another dirty trick.
The result of the execution should look like follows.
print("Extra height: \(navigationController!.navigationBar.lagreTitleHeight)")
Extra height: 52.0
Seems like calling navigationBar.sizeToFit() forces the navigationBar to adjust its size as it shows large title. Therefore you can calculate top safe area easily. The solution that we came up with is next:
self.navigationController?.navigationBar.sizeToFit()
let navigationBarOffset = navigationController?.navigationBar.frame.origin.y ?? 0
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let offset = -navigationBarOffset - navigationBarHeight
if collectionView.contentOffset.y > offset {
let contentOffset = CGPoint(x: 0, y: offset)
collectionView.setContentOffset(contentOffset, animated: true)
}
As I understand it, you have your tableView origin under the navigationBar, at y = 0 in your UIViewController's view.
Your tableView should have its top bound fix to the top layout guide, or use the new safe area. That way you won't have to programmatically calculate what's the size of the navigationBar.
If you never used it, take a look at auto-layout.
You can simply scroll to the top of the table view without knowing the size of the navigation bar. UITableView comes with an API for that.
Option 1
// Get the first index path. You might want to check here whether data source item exists.
NSIndexPath *firstIndexPath = [NSIndexPath indexPathForRow:0
inSection:0];
// Scroll to it. You can play with scroll position to get what you want
[tableView scrollToRowAtIndexPath:firstIndexPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
Option 2
CGPoint offset = CGPointMake(0.0, -tableView.contentInset.top);
[tableView setContentOffset:offset animated:YES];
I just experienced this same issue.
-(void)layoutSubviews{
[super layoutSubviews];
CGRect rectStatusBar = [[UIApplication sharedApplication] statusBarFrame];
if (rectStatusBar.size.height==44.f) {
//statusBar shows
}else{
if (#available(iOS 11.0, *)) {
for ( UIView*view in self.subviews) {
if ([NSStringFromClass(view.classForCoder) isEqualToString:#"_UINavigationBarContentView"]) {
view.frame = CGRectMake( 0,20,view.frame.size.width,44);
}
else if ([NSStringFromClass(view.classForCoder) isEqualToString:#"_UIBarBackground"]) {
view.frame = CGRectMake(0,0,view.frame.size.width, 64);
}
}
}
}
}

iOS11 customize navigation bar height

First of all, thank you for coming here and help solving my problem. Thank you!!!
In iOS11 beta6, sizeThatFits: seems to not work on UINavigationBar. I notice that UINavigationBar structure has changed by Reveal my app.
I have tried my best to change custom navigation bar's height. But it seems always to be 44, and it works before iOS11.
- (CGSize)sizeThatFits:(CGSize)size {
CGSize newSize = CGSizeMake(self.frame.size.width, 64);
return newSize;
}
Oddly, I just log its frame in didMoveToSuperview method, its height is 64, but I really see that is 44 in Reveal and app.
I have no idea about this... Help me please.. Thank you.
Update:
I found that about my custom navigation bar LayoutConstraints log in console like this :
"<NSAutoresizingMaskLayoutConstraint:0x604000495ae0 FDCustomNavigationBar:0x7fe2f01399d0.(null) == 42>",
"<NSAutoresizingMaskLayoutConstraint:0x604000495b30 FDCustomNavigationBar:0x7fe2f01399d0.height == 44>"`
bug I even no use auto layout in my navigation bar. What's wrong with it?
Update 8/28 :
I have set my custom navigation bar's subviews frame at navigation bar's -
layoutSubviewsmethod.
- (void)layoutSubviews {
[super layoutSubviews];
self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 64);
for (UIView *view in self.subviews) {
if([NSStringFromClass([view class]) containsString:#"Background"]) {
view.frame = self.bounds;
} else if ([NSStringFromClass([view class]) containsString:#"ContentView"]) {
CGRect frame = view.frame;
frame.origin.y = 20;
frame.size.height = self.bounds.size.height - frame.origin.y;
view.frame = frame;
}
}
}
but the navigation bar will cover view controller's view. How can I fix that?
Since iOS 11 UINavigationBar fully supports Auto Layout (this is the reason why you're seeing its constraints). I've opened a radar to Apple because I thought that setting a height constraint to the titleView would have adjusted the navigation bar height accordingly. However this is what Apple replied:
Full support for auto layout does not imply that your view can influence other aspects of the layout of the navigation bar – in particular, the navigation bar enforces its own height and does not allow the title view or other custom views to exceed the height of the navigation bar. We are continuing to work on this issue, and will follow up with you again.
As of today the radar is still open.
Hello I just experienced this same issue.
Now the top layout guide is deprecated on iOS 11. You have to reference the safeAreaLayoutGuide in your constraints.
Here's an example in swift
if #available(iOS 11, *) {
let guide = self.view.safeAreaLayoutGuide.topAnchor
let height = (self.navigationController?.navigationBar.frame.height)! - CGFloat(12)
NSLayoutConstraint.activate([
self.yourTableView.topAnchor.constraint(equalTo: guide, constant: height)
])
}
As you can see your view's top anchor should match the safeAreaLayoutGuide top anchor. In this example I'm using the variable height to create the new constraint. The variable height contains the navigation bar height minus a constant.
You should try by changing the height value.
Hope this helps you.

How to determine UINavigationBar size before device orientation changes

I'm writing an app in Objective-C using Xcode 6 and iOS 8. The app needs to be able to be deployed on an iPhone 5, 6, or 6+.
If you want to get straight to answering my question, jump down to the last sentence. If you want to understand why I have the question I do, or maybe how I can alter my UI layout in order to solve my problem another way, read on.
In one of my view controllers, I have a scroll view whose top is constrained to the bottom of the navigation bar, and whose bottom is constrained to the top of a table view. The table view's bottom is constrained to the bottom of the view controller's main view (i.e. to the bottom of the phone).
The scroll view contains subviews that expand/contract when the user taps on them. I want the scroll view to grow as its subviews grow, but obviously I don't want the scroll view to grow off screen because it looks bad and because it would cause unsatisfiable constraints (the table view's top--which is constrained to the bottom of the scroll view--would cross below its bottom--which is constrained to the bottom of the main view...this causes an error). So, I use the following code to make the scroll view resize itself according to its subviews sizes without growing right off the screen:
// The max height before the scroll view would go off screen, which would
// mess up the table view's constraints and cause all sorts of problems
CGFloat maxHeight = self.view.size.height
- self.navigationController.navigationBar.frame.size.height
- [UIApplication sharedApplication].statusBarFrame.size.height;
// The height of all the subviews in the scroll view.
CGFloat height = _scrollContentView.frame.size.height;
if (height > maxHeight) {
height = maxHeight;
}
self.scrollViewHeightConstraint.constant = height;
Now for the fun part. Originally, I called this code to re-evaluate and reset the size of the scroll view whenever I rotated the device from portrait to landscape, or vice versa. However, when I would rotate the phone from portrait to landscape, I was getting constraints errors. I determined that it was because I was calling this code after the rotation, when the main view's height was smaller, but the scroll view's height was still large (causing the table view's top to go below the bottom, etc. as I explained before). So, I just moved the code to be called before the rotation (I called the code in the viewWillTransitionWithSize:withTransitionCoordinator: method). This all makes sense so far.
However, now, the problem is that the navigation bar's height changes when the rotation occurs, but the viewWillTransitionWithSize:... method does not include any details on this change (it only gives the new size that the main view will be when rotation is completed, not the new size the navigation bar will be as well).
So, I need someway to determine the new size of the navigation bar before the device's orientation actually changes (just like I can determine the main view's new size before the device's orientation actually changes using the viewWillTransitionWithSize:... method).
Any ideas? TIA!
So, here's my work around in its simplest form:
/*
* This method gets called when the device is about to rotate.
*/
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
// Set the scroll view's height to 0 to avoid constraints errors as described
// in the question.
self.scrollViewHeightConstraint.constant = 0;
}
/*
* At the point when this method gets called, the device rotation has finished altering
* the frames of the views in this view controller, but the layout has not finished
* so nothing has changed on screen.
*/
- (void)viewWillLayoutSubviews
{
// The max height before the scroll view would go off screen, which would mess up
// the table view's constraints and cause all sorts of problems
CGFloat maxHeight = self.view.size.height
- self.navigationController.navigationBar.frame.size.height
- [UIApplication sharedApplication].statusBarFrame.size.height;
// The height of all the subviews in the scroll view.
CGFloat height = _scrollContentView.frame.size.height;
if (height > maxHeight) {
height = maxHeight;
}
// Reset the scroll view's height to the appropriate height.
self.scrollViewHeightConstraint.constant = height;
[super viewWillLayoutSubviews];
}

How to maintain a minimum height of content size of UIScrollView?

I have a UIScrollView A (in fact a UICollectionView) filling the screen inside a UINavigationController B. The controller B's adjustScrollViewInsets is set to true.
I want to hide the navigation bar when user scrolls up, and show it when down. Following is my code:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentSize.height - scrollView.frame.size.height && self.lastContentOffset > scrollView.contentOffset.y) {
// dragging down
if self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
} else if (self.lastContentOffset > 0 && self.lastContentOffset < scrollView.contentOffset.y) {
// dragging up
if !self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
self.lastContentOffset = scrollView.contentOffset.y
}
Now the problem is, since the screen of iPhone 6+ is too large, the contentSize of the scroll view A is smaller than its frame(i.e. the full screen frame) when the navigation bar is hidden. In such circumstance, the scroll view will not be scrollable, and the navigation bar will never be back again.
I want to manually maintain the height of the contentSize of A to screen at least height + 1, but don't know how to do this. Could anyone help? Or provide a better solution?
BTW, I am using iOS 8 and Swift.
Lets say you need to keep the minimum content size of scroll view to 100(of course this will be dynamic and vary according to device)
NSInteger minScrollViewContentHeight = 100;
After populating the scroll view with content, you need to check if the scroll view's content size is less than minimum required scroll views content size. If its lesser than the required content size than you need to set the minimum content size of the scroll view as follows -
if(scrollView.contentSize.height < minScrollViewContentHeight)
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, minScrollViewContentHeight)];
Off the top of my head (I'm on a phone), contentSize is not read-only I think.
How about changing it manually to the desired amount depending on the circumstances of scrolling direction etc?
Something like:
IF navbar is hidden THEN contentSize = whatever
An option would be to use the appearance and disappearance of cells to trigger the show/hide.
Use the delegate methods collectionView:willDisplayCell:forItemAtIndexPath: and collectionView:didEndDisplayingCell:forItemAtIndexPath: to detect movement. You can work out the direction from the index change of the cells being shown or removed. If you cannot scroll off screen then nothing happens.
You have to change no offset (which is actually just scrolling position), but contentSize itself. That means, that when you hide navigation bar, increase contentSize by navigation height (don't remember numbers) and when you show navigation bar, decrease contentSize. Or... Use AutoLayout and layoutIfNeeded method after showing/hiding navigation bar.
I stumbled upon a similar problem. I needed a minimum scrollable area for a tableview i was using.
ScrollView might be a bit easier since you can directly modify the contentView size.
If you're using autoLayout, try adding equal heights constraint between the contentView and the scrollView itself. Something along the lines of contentView.height = scrollView.height + scrollMin;
For my tableView i had to subclass UITableView and override the contentSize setter.
#define MIN_SCROLL 60
- (void)setContentSize:(CGSize)contentSize {
//take insets into account
UIEdgeInsets insets = self.contentInset;
CGFloat minHeight = self.frame.size.height - insets.top - insets.bottom + MIN_SCROLL;
if(contentSize.height < minHeight) {
contentSize.height = minHeight;
}
[super setContentSize:contentSize];
}

iOS7: UICollectionView appearing under UINavigationBar

I've been building on iOS 7 for a while now but I've yet to get this solved, I have a number of views with autolayout enabled that were created in Storyboard and are displayed with a standard UINavigationController. The majority are fine, but the ones based on UICollectionView always place themselves under the navigation bar, unless I set the translucency to NO. I've tried the edgesExtended trick but that doesn't seem to solve it, I don't necessarily mind having the translucency off but I'd like to solve it cleaner.
FYI if your UICollectionView is the root view in your view controller's hierarchy and your view controller has automaticallyAdjustsScrollViewInsets set to YES (this is the default), then the contentInset should update automatically.
However, the scrollview's contentInset is only automatically updated if your scrollview (or tableview/collectionview/webview btw) is the first view in their view controller's hierarchy.
I often add a UIImageView first in my hierarchy in order to have a background image. If you do this, you have to manually set the edge insets of the scrollview in viewDidLayoutSubviews:
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat top = self.topLayoutGuide.length;
CGFloat bottom = self.bottomLayoutGuide.length;
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
}
I had this problem before, just set the edge insents of the collection view with a top margin:
[self.myCollectionVC.collectionView setContentInset:UIEdgeInsetsMake(topMargin, 0, 0, 0)];
Where topMargin is the size of the nav bar, or whatever point you want the collection to start scrolling.
In this way, your collection view will start scrolling just below the navigation bar, and at the same time it will fill the whole screen and you will see it if your nav bar is translucent.
I had this problem after ios 11, just set the contentInsetAdjustmentBehavior of UICollectionView to never:
self.collectionView.contentInsetAdjustmentBehavior = .never
I am using swift and xcode 7.3.1. I solved it by going to story board and selecting my Navigation Controller and then unchecking "Extend Edges" "Under Top Bards".
-(void) viewDidLoad{
[super viewDidLoad];
self.automaticallyAdjustsScrollViewInsets = NO; //added important
}
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat top = self.topLayoutGuide.length;
CGFloat bottom = self.bottomLayoutGuide.length;
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
}

Resources