1

I am adding a UINavigationBar manually in a View Controllers loadView.

I am using Cartography (https://github.com/robb/Cartography) to layout views with Auto Layout.

self.edgesForExtendedLayout = UIRectEdge.None
        let navBar = UINavigationBar()
        navBar.delegate = self
        view.addSubview(navBar)

        constrain(navBar) { bar in
            bar.top == bar.superview!.top
            bar.centerX == bar.superview!.centerX
            bar.width == bar.superview!.width
        }

Delegate method:

public func positionForBar(bar: UIBarPositioning) -> UIBarPosition {
        return .TopAttached
    }

Yet the result is small bar, not the extended version under the status bar.

Zdeněk Topič
  • 762
  • 4
  • 29

1 Answers1

6

If you are adding a navigation bar programmatically you need to be aware that edgesForExtendedLayout and any bar positioning API will not be relative to the layout guides of your interface. Since you have now indicated you want to manage this bar, the system can't enforce whether it should be under the status bar or not. What you need to do is configure your constraints such that the bar will always be positioned relative to the top layout guide.

So for starters lets head to your loadView method:

let bar = UINavigationBar()
bar.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(bar)
self.navBar = bar
let topLayoutGuide = self.topLayoutGuide

We now need to make sure that the navigation bar's bottom is positioned relative to the layout guide. So all we do is say that the bottom of the navigation bar = layoutGuides's top + 44. Where 44 is a decent height for a navigation bar. Remember the layout guide can change when you are in a phone call so its important to always use the layout guide and never hardcode the status bar height.

Approach Using Cartography

constrain(navBar) { bar in
    bar.top == bar.superview!.top
    bar.width == bar.superview!.width
    bar.bottom == topLayoutGuide.top + 44
}

Approach Using NSLayoutConstraints

let navBarTopConstraint = NSLayoutConstraint(item: bar, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0)
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[bar]-0-|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["bar":bar])
let navBarBottomConstraint = NSLayoutConstraint(item: bar, attribute: .Bottom, relatedBy: .Equal, toItem: topLayoutGuide, attribute: .Top, multiplier: 1, constant: 44)
self.view.addConstraints([navBarTopConstraint,navBarBottomConstraint,horizontalConstraints]))

Voila! You now have a custom navigation bar in a couple of lines that responds to the status bar

Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
  • Could you elaborate more? It solved it for me and I use this in several projects @ZdeněkTopič – Daniel Galasko Jan 20 '15 at 10:03
  • oh, i see .. you are setting the height manually .. i was kinda hoping for some solution where ios decides the height .. – Zdeněk Topič Jan 21 '15 at 17:55
  • @ZdeněkTopič iOS can only do so much for you. If you want to add your own views you have to enforce your own constraints. Please consider accepting my answer if it meets your criteria – Daniel Galasko Jan 21 '15 at 21:01
  • Ok, can you edit it to actually underlap the status bar? – Zdeněk Topič Jan 29 '15 at 13:02
  • I can't say this solution is satisfying. First, it doesn't stick to `MVC`, as you're handling view (contraints, frames should rather be implemented in views) in controller (mentioned `self.topLayoutGuide`). Secondly, hardcoding anything is a bad practice. Navbar has different heights in iPhone portrait and landscape modes and the solution is broken in this case. Ofc it's a great starting point for own research, but I wouldn't say that's good way to keep in final product. Anyway, nicely written answer. – Nat Jun 22 '15 at 22:05
  • @Vive interesting criticism. I definitely agree, the exercise of extracting the navBar into a custom view is rather trivial. But there isn't anything wrong with letting a view controller manage the frames of its contents as that is one of its responsibilities. it can get messy when you start to add more views. But to save a few lines I don't really see the benefit. Furthermore the height is always 44, it's the status bar that is hidden in portrait. I don't see another option but hardcoding height? – Daniel Galasko Jun 23 '15 at 05:29
  • @DanielGalasko Unfortunately I'm not so advanced in Swift to do present a solution without brainstorming for a while. However in Obj-C I've used this solution in a category: http://stackoverflow.com/a/21541533/849616, which isn't also perfect, but at least do not use hardcoded values. In my opinion the frames should be handled via view, and the controller should be independent from any outlook handling. Usually I try to enforce proper architecture in 100% as it's easier for me to maintain it later (especially when one doesn't use storyboards and prefer inheritance and reuse of views). – Nat Jun 23 '15 at 07:14
  • @Vive thats not exactly a good solution since now the child controller needs to traverse up the hierarchy and retrieve information from its parent. Its a better idea to customise this from the container since that way its also easier to turn off for particular controllers. A view should never be responsible for configuring its position in a container. The container should perform that... – Daniel Galasko Jun 23 '15 at 08:16
  • @DanielGalasko Right, that's why we're not setting self.frame but self.subview.frame (or constraints adequately). The frame of the view is handled indeed in controller. In my case I'm doing it via setting proper autoresizingMask in loadView method, and this solution doesn't break the matter you're describing. As to traversing that's truth, but I prefer to pass the value (99% cases passing from given controller to it's view) than to hardcode it and wait until Apple changes the height and breaks whole app. – Nat Jun 23 '15 at 08:25
  • @Vive i hear you but that won't break the app. Strictly speaking since the OP wants a custom navigationBar perhaps they want to enforce a consistent height. Otherwise the suggestion would simply be to use the stock standard bar and your problems are solved. Flipboard is a good example of a custom bar. – Daniel Galasko Jun 23 '15 at 08:46
  • You can also directly use `topLayoutGuideCartography` instead of dealing with magic numbers like `44` – erenkabakci Nov 17 '16 at 00:25
  • @erenkabakci `topLayoutGuideCartography` is not a part of UIKit so I am not sure exactly to what you refer – Daniel Galasko Nov 17 '16 at 13:08
  • @DanielGalasko `topLayoutGuideCartography` is a member of a `UIViewController` extension in cartography. I suggested that since the question and your answer uses cartography explicitly. – erenkabakci Nov 17 '16 at 17:01
  • oh right I see @erenkabakci but that doesnt solve the solution. 44 is just the default height of a navigation bar. I am still using the topLayoutGuide. Your solution is to just replace topLayoutGuide with topLayoutGuideCartography... The code doesnt change much with that change – Daniel Galasko Nov 18 '16 at 11:47
  • @DanielGalasko Well, if you use `topLayoutGuideCartography` you don't need to manually calculate navbar height by adding `44` the desired top layout guide starts from the bottom of the navbar. that is why. – erenkabakci Nov 18 '16 at 13:18
  • @erenkabakci I think we missing one another here. Please read the actual question and try and achieve it using your solution, post an answer and let's take it from there – Daniel Galasko Nov 23 '16 at 17:37