Implementing hidesNavigationBarWhenPushed on UIViewController

Written by gontovnik | Published 2018/06/23
Tech Story Tags: ios | uinavigationbar | navigation-bar | uiviewcontroller | view-controller

TLDRvia the TL;DR App

It’s been a while since my last post.

I am back and hoping to dedicate more time to writing tutorials and open sourcing interesting things.

What this post is about?

In this post I want to go through the recently published HidesNavigationBarWhenPushed library and explain the motivation behind that, as well as the implementation.

hidesNavigationBarWhenPushed in action (With interactive pop gesture recognizer)

Motivation

While hacking on various projects, time to time I get designs where view controller A has navigation bar visible, and view controller B has it hidden. It was always a pain to make it work.

The simplest solution would be:

And it would work like this:

That did not quite work, since the key requirement was to keep navigation bar title and button transitions the same, as if the navigation bar was visible.

So I thought…

What if at the time of push or pop I could hide the existing navigation bar, and show the fake one instead?

Investigation

Before jumping straight to the coding, I decided to better understand how UINavigationBar works behind the scene. That process involved two steps:

  1. Reading through the UINavigationBar runtime header
  2. Checking what is inside navigation bar via Debug View Hierarchy

That made me understand two things:

  1. Navigation bar itself is always positioned with origin.y equal to status bar height, but it’s childs are laid out with negative origin.y, to simulate as if the navigation bar had origin.y equal to 0.
  2. Navigation bar contains a child of class _UIBarBackground, which is responsible for drawing the actual background. In runtime headers I found that there is a getter _backgroundView.

The position of navigation bar is offset vertically by the height of the status bar.

The vertical position of the background view is negative, to simulate that navigation bar is position with origin.y equal to 0.

Implementation

After the investigation I had enough information to start hacking on that.

I developed a mini-plan on how it should work. There are two cases I had to worry about:

  1. Transition from view controller, which has navigation bar visible, to the one which has navigation bar hidden, and back
  2. Transition from view controller, which has navigation bar hidden, to the one which has navigation bar visible, and back

Let’s go through these cases in a little bit more detail.

From visible to hidden and back

  1. When I push from the current view controller, which has the navigation bar visible, to the target view controller, which has the navigation bar hidden, I copy the navigation bar and add it to the current view controller’s view and I hide the background view on the original navigation bar, which is the subview of the navigation controller’s view. In this case I can guarantee, that the navigation bar background remains only in the current view controller, but not in the target view controller. See implementation.
  2. When I pop from the current view controller, which has the navigation bar hidden, to the target view controller, which has the navigation bar visible, I wait for the navigation transition to finish. At the end of the transition I remove the fake navigation bar from the target view controller’s view and unhide the background view on the original navigation bar. See implementation.

From hidden to visible and back

  1. When I push from the current view controller, which has the navigation bar hidden, to the target view controller, which has the navigation bar visible, I copy navigation bar, but I add it to the target view controller only when viewWillAppear is called. At the end of this push transition I have to remove the fake navigation bar from the target view controller and unhide the background view on the original navigation bar. See implementation.
  2. When I pop from the current view controller, which has the navigation bar visible, to the target view controller, which has the navigation bar hidden, I copy the navigation bar and add it to the current view controller, and hide the background view on the original navigation bar. In case if transition is failed and we returned back to the current view controller, I add logic to remove fake navigation bar and unhide the background view on the original navigation bar on viewWillAppear of the current view controller. See implementation.

Bugs and bug-fixes

Surprisingly, there was only a single bug I’ve encountered and had to fight against.

Since _backgroundView is the private API and is managed by Apple, there were cases when this view was hidden or unhidden by Apple. To prevent that and keep it in sync with my state, I’ve added logic to observe hidden flag on background view and override it with my value, if it has unexpectedly changed. See implementation.

Conclusion

To be honest, it is risky to include hack like this into the production application, since it uses Apple’s internal API which can change at any point of time. On the other hand, I had this logic in production for more than a year and did not encounter any bugs.

If you have any questions, please drop me a line!

Thank you for reading! If you liked this post, please hit ‘Clap’ (the 👏 button), so other people can read it too! 😉

Hit me on Twitter, if you want to chat.


Published by HackerNoon on 2018/06/23