Extending UINavigationBar with interactable UIView below it

In recent years, we have seen some great examples of extending the navigation bar to add helpful functionality below it in apps such as Telegram, GitHub, and others.

Here at Shareup, we strive to make sharing easy and convenient…and private. To ensure the privacy of the things you share, we encrypt everything locally on your device. Depending on how large the files you share are, this can take a few seconds. So, we thought adding an encryption progress bar under the navigation bar would be a wonderful way to show our users how far along the encryption process is after sharing files.

In this simple example, we will tell you how we did this by adding a similar view to an app that prepares tea☕️.

We want to attach our custom UIView to the UINavigationBar to be present on every UIViewController pushed to the navigation stack. So, the first step is straightforward. Add the following code to your root view controller to attach the progress view to your navigation controller’s navigation bar.

override func viewDidLoad() {
  super.viewDidLoad()
  if let navigationBar = navigationController?.navigationBar {
    navigationBar.addSubview(progressBarView)
    progressBarView.translatesAutoresizingMaskIntoConstraints = false
    
    NSLayoutConstraint.activate([
      progressBarView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
      progressBarView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
      progressBarView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor),
      progressBarView.heightAnchor.constraint(equalToConstant: 36),
    ])
  }
}

And we would rather hide it initially.

progressBarView.isHidden = true

Since we have this fabulous tea-looking button, let’s add a simple action to show our tea preparation progress bar.

makeTeaButton.addAction(
  UIAction(handler: { [weak self] _ in
    self?.progressBarView.isHidden = false
  }),
  for: .touchUpInside
)

You can tell that something is not right so far. Since our progress bar overlaps the content of the UIViewController. Furthermore, you will notice that it not only overlaps the content of the UIViewController, but also does not accept touches, and you can easily select the row underneath it.

Let’s now fix the overlapping issue. To do that, we need to increase additional safe area insets in every UIViewController affected by this problem.

makeTeaButton.addAction(
  UIAction(handler: { [weak self] _ in
    self?.progressBarView.isHidden = false
    self?.additionalSafeAreaInsets.top = 36 // This the height of this particular view, yours might differ
  }),
  for: .touchUpInside
)

Remember to set insets to zero once you finish displaying your custom view.

self?.additionalSafeAreaInsets = .zero

The last issue to address is handling gestures. Despite looking solid, our custom view does not receive any events. This is obvious since it is located outside the frame of the UINavigationBar.

Our solution will be easy to understand if you have heard about hit testing. Otherwise, look at it as follows: we force all of the subviews of the UINavigationBar to check whether the gesture took place inside its frame so that we won’t miss any of it.

public extension UINavigationBar {
  override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews where (!subview.isHidden && subview.alpha >= 0.01) {
      let convertedPoint = subview.convert(point, from: self)
      if subview.point(inside: convertedPoint, with: event) { return true }
    }
    return false
  }
}

If you want to play with the code more, you can find the complete project here.

If you want to be among the first people to try Shareup - sign up for closed alpha by clicking the button below.

Get early access

🚀🆙