'Unable to hide the navigationBar when embedding SwiftUI in UIKit

I am trying to hide the navigationBar when putting some SwiftUI inside of a UIKit UIViewController:

override func viewWillAppear(_ animated: Bool) {
   super.viewWillAppear(animated)
   self.navigationController?.setNavigationBarHidden(true, animated: animated)

But it does not go away. When I take away the SwiftUI however, it works. Does anyone know how to solve this?

Edit:

I am instantiating a view like this:

let controller = UIHostingController(rootView: view())

where view is the SwiftUI and then adding this to the UIView() as you would any UIKit element.



Solution 1:[1]

UIHostingViewController respects the navigationBarHidden value of your SwiftUI view. You can either call .navigationBarHidden(true) at the end of your SwiftUI view, or you can use the custom UIHostingController subclass shown in the example below.

Solution:

import SwiftUI
import UIKit

class YourHostingController <Content>: UIHostingController<AnyView> where Content : View {

  public init(shouldShowNavigationBar: Bool, rootView: Content) {
      super.init(rootView: AnyView(rootView.navigationBarHidden(!shouldShowNavigationBar)))
  }

  @objc required dynamic init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
}

Example of usage:

let hostVc = YourHostingController(shouldShowNavigationBar: false, rootView: YourSwiftUIView())

Solution 2:[2]

Ran into this problem yesterday, too.

I am presenting a modal UINavigationController with a UIViewController as rootViewController, which embeds a SwiftUI View via UIHostingController.

Setting the usual setNavigationBarHidden in viewDidAppear of the UIViewController stops working as soon as the SwiftUI View is embedded.

Overview:

Root ViewController: setNavigationBarHidden in viewWillAppear

Navigation Bar Visible:
UINavigationController > root UIViewController > embedded UIHostingController

Navigation Bar Invisible:
UINavigationController > root UIViewController > no UIHostingController

After some debugging I realized that the UIHostingController itself calls setNavigationBarHidden again.

So the reason for this problem is, that the UIHostingControllers alters the surrounding UINavigationController's UINavigationBar.

One easy fix:

Set the Navigation Bar property in the first presented SwiftUI View that is embedded by your UIHostingController.

    var body: some View {
        MyOtherView(viewModel: self.viewModel)
            .navigationBarHidden(true)
    }

This will revert the adjustment SwiftUI and the UIHostingController are trying to apply to your surrounding UINavigationController.

As there is no guarantee about the interaction between SwiftUI and UIKit (that it uses underlying UIKit), I would suggest keeping the setNavigationBarHidden in the surrounding viewDidAppear together with this modifier, too.

Solution 3:[3]

Hiding navigation bar from a class that is extending UIHostingController seems to work when setNavigationBarHidden is called in viewDidAppear instead of viewWillAppear.

override func viewDidAppear(_ animated: Bool) {
    navigationController?.setNavigationBarHidden(true, animated: false)
    super.viewDidAppear(animated)
}

Solution 4:[4]

Using the modifier .navigationBarHidden(true) did not work in our case. It had no effect.

Our solution is to subclass UIHostingController and don't let it access the UINavigationController at all. For example:

import UIKit
import SwiftUI

final public class RestrictedUIHostingController<Content>: UIHostingController<Content> where Content: View {

    /// The hosting controller may in some cases want to make the navigation bar be not hidden.
    /// Restrict the access to the outside world, by setting the navigation controller to nil when internally accessed.
    public override var navigationController: UINavigationController? {
        nil
    }
}

Note that this solution relies on underlying code in UIKit and SwiftUI accessing the UINavigationController and setting the navigation bar hidden state based on the UIViewController.navigationController-property. This may break in the future if Apple decides to change on this assumption.

Solution 5:[5]

I want to include my approach here just in case someone find it useful when working with SwiftUI. I found out that the problem was that UIHostingController was overriding something on my declare of

navigationController?.setNavigationBarHidden(true, animated: false)

So i just created a custom UIHostingController and used viewWillAppear(_ animated:Bool):

class UIHostingViewControllerCustom:UIHostingController<YourView>{
  override func viewWillAppear(_ animated: Bool) {
    navigationController?.setNavigationBarHidden(true, animated: false)
  }
}

Then when you are adding that UIHostingController into your ViewController:

let hostingController = UIHostingViewControllerCustom(rootView: YourView())
hostingController.view.backgroundColor = .clear
addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)
hostingMapView.didMove(toParent: self)

//Constraints
hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo:  self.view.safeAreaLayoutGuide.topAnchor, constant: -view.safeAreaInsets.top).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -view.safeAreaInsets.bottom).isActive = true

Solution 6:[6]

In my case, I had to use this UIHostingController subclass.

class NavigationBarHiddenUIHostingController<Content: View>: UIHostingController<Content> {
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    if navigationController?.isNavigationBarHidden == false {
      navigationController?.isNavigationBarHidden = true
    }
  }
}

Solution 7:[7]

Nothing worked for me so I added an observer to hide navigationBar in the parent view:

    private var observer: NSKeyValueObservation?


    override func viewDidLoad() {
        super.viewDidLoad()

        observer = navigationController?.observe(
            \.navigationBar.isHidden,
            options: [.new]
        ) { [weak self] _, change in
            guard change.newValue == false else { return }
            self?.navigationController?.navigationBar.isHidden = true
        }
    }

Solution 8:[8]

Unfortunately, if you are making UIHostingViewController without UINavigationController, you would need to make some adaptions to the frame itself(actually to reduce its topAnchor to 48). It appears that navigationBar spacing shows up only on next viewWillAppear and layout of subviews.

Here is the solution that I have used for my UIHostingViewController.

Firstly, I have made function(inside of my UIHostingViewController) that would set origin(x,y) of my inner subview and set the constraints to self.view. It has condition(to not do that every time, only when navigation bar spacing shows up):

    private var savedView: UIView?

private func removeAdditionalTopSpacing() {
    if view.subviews.count == 0  {
        return
    }
    
    var widgetFrame = view.subviews[0].frame
    let widgetStartingPoint = widgetFrame.origin.y
    widgetFrame.origin.y = 0
    widgetFrame.origin.x = 0
    
    self.view.subviews[0].frame = widgetFrame
    self.view.subviews[1].frame = widgetFrame
    
    if widgetStartingPoint > 0 {
        self.savedView = self.view

        self.savedView?.translatesAutoresizingMaskIntoConstraints = false
        self.savedView?.widthAnchor.constraint(equalTo: self.savedView!.subviews[0].widthAnchor).isActive = true
        self.savedView?.heightAnchor.constraint(equalTo: self.savedView!.subviews[0].heightAnchor).isActive = true
        self.savedView?.centerXAnchor.constraint(equalTo: self.savedView!.subviews[0].centerXAnchor).isActive = true
        self.savedView?.centerYAnchor.constraint(equalTo: self.savedView!.subviews[0].centerYAnchor).isActive = true

        self.view = self.savedView
        self.view.setNeedsLayout()
        self.view.layoutIfNeeded()
    }
}

Important note: Reason why I have saved current view inside of private variable savedView is because of his existence and memory release. In this way it won't be lost when removeFromSuperView got called. There are always 2 subviews of UIHostingViewController.view. One for content and another one for hit range. Both are moved for 48 points down when navigation bar spacing shows up.

There are two places where I have called it: viewDidAppear() and viewDidLayoutSubviews():

public override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    removeAdditionalTopSpacing()
}

public override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    removeAdditionalTopSpacing()
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Frederik Winkelsdorf
Solution 3 Mika
Solution 4 Gustaf Rosenblad
Solution 5 Wilson Muñoz
Solution 6 getogrand
Solution 7 Amir Khorsandi
Solution 8 Dharman