14

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];
SAHM
  • 4,078
  • 7
  • 41
  • 77
  • you don't need to know about the current layout to do so, just make the first cell visible, if you want to scroll to the top ([`scrollToRowAtIndexPath:atScrollPosition:animated:`](https://developer.apple.com/documentation/uikit/uitableview/1614997-scrolltorowatindexpath)), that should be enough. – holex Oct 23 '17 at 07:23
  • Thank you, and I had tried that. However, my use case requires me to scroll to the point where the large nav title is showing, and this only scrolls to the point where the top row is showing. To get the large title to show, I need the 'extra height' of the nav bar - the area that shows the large title. I have tried to think of another way to do this, but at this point, I do believe I need that height. – SAHM Oct 23 '17 at 13:46
  • there is a golden rule here: if the answer to your _question_ is to track system generated layouts' boundaries to adjust your own layout – you are probably raising the wrong _question_. – holex Oct 23 '17 at 14:53
  • @holex I get it, which is why I have been surprised to have this issue. I am hoping to find that this is the case for my current issue. However, in this particular case, so far, I have not been able to find any other way to scroll to exactly the point where the title is pulled down, and not past its top - and this is what I need to happen for my use case, which I do not believe is unreasonable. I am scrolling to show the large title as a result of changing the view in a container. – SAHM Oct 23 '17 at 15:24
  • @holex how would you scroll a tableview to show the top of the large title? Just curious, because I think we both agree I am missing something. – SAHM Oct 24 '17 at 02:11

5 Answers5

12

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.

debugger preview of the navigation bar

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

  1. 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.
  2. Finds the lowest subview in the in the navigation bar.
  3. Maps the resulted subview frame to the frame size.
  4. 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
Rafał Sroka
  • 39,540
  • 23
  • 113
  • 143
Kamil Szostakowski
  • 2,153
  • 13
  • 19
  • YES! Thank you! And as a side bar note, you want to calculate the extra height when the view controller first displays, before the extra-height part of the nav bar is scrolled under. Thank you! – SAHM Oct 28 '17 at 22:16
  • PS I just offered a bounty on this question as well if you care to take a shot. You apparently know what you are doing! https://stackoverflow.com/questions/46779985/uitableviewcell-hide-separator-using-separatorinset-fails-in-ios-11 – SAHM Oct 28 '17 at 22:21
2

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)
}
Oleg Kosenko
  • 105
  • 5
1

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.

Damien
  • 3,322
  • 3
  • 19
  • 29
  • I use auto layout constantly. I have switched to iOS 11, safe areas, and large navigation titles. I think if you reread my question you will see that I am not trying to calculate the size of the nav bar - my nav bar is placed perfectly via auto layout. I am trying to calculate an offset for a scrollView under certain conditions. – SAHM Oct 26 '17 at 15:33
  • As I understand the offset you want for the scrollview is for the scrollview content not to be hidden, right ? – Damien Oct 26 '17 at 15:40
  • 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 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. – SAHM Oct 26 '17 at 16:00
  • I didn't try it but I assume using auto-layout with top layout guide or safe-area would place your view just under the whole bar, large title included. That's what I understood from wwdc videos and documentation. – Damien Oct 26 '17 at 16:04
  • It does. That's not my issue, however. The large title scrolls under the "regular height" nav bar when the tableView is scrolled to show more rows at the bottom. I need to (programmatically) scroll to the top of the tableView and then some so that the large title will come back down. – SAHM Oct 26 '17 at 18:06
  • That's why you should constraint your scroll view top edge to the bottom of the large title bottom edge. That's why auto-layout is for, you shouldn't specify hard coded values. I can't help you with the large title height. – Damien Oct 27 '17 at 08:19
  • I am using a container view with a scrollview as the child. However, this same exact issue exists for a UITableView whose top is constrained to the top of the safe view. I think maybe someone else can probably help a bit more with this. – SAHM Oct 28 '17 at 21:44
0

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];
Rafał Sroka
  • 39,540
  • 23
  • 113
  • 143
  • 1
    Thank you, and I had tried that. However, my use case requires me to scroll to the point where the large nav title is showing, and this only scrolls to the point where the top row is showing. To get the large title to show, I need the 'extra height' of the nav bar - the area that shows the large title. I have tried to think of another way to do this, but at this point, I do believe I need that height. – SAHM Oct 23 '17 at 13:45
  • What if you used `UITableViewScrollPositionBottom` in `scrollToRowAtIndexPath`? – Rafał Sroka Oct 23 '17 at 14:58
  • I did try that and unfortunately it did not work. But thanks for the suggestion. – SAHM Oct 23 '17 at 15:20
0

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);
                        }
                    }
                }
            }
        }
vipinsaini0
  • 541
  • 1
  • 7
  • 26
  • I'm not sure exactly what your issue is, but using hardcoded values is what I am trying to get away from, so unfortunately this is not a solution. – SAHM Oct 25 '17 at 16:28