'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 |