Reputation: 673
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.
Upvotes: 53
Views: 21626
Reputation: 1411
Ran into this where I have a SwiftUI view using navbars embeded in a UIHostingController
that needed to work for iOS 15 and up. The UIHostingController
was also in a UINavigationController
. The result was two navbars appearing, which I needed to fix.
The resulting code was for the UIHostingController
final class HostingViewController: UIHostingController<TheSwiftUIView> {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
}
Then TheSwiftUIView
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
// navigation stack code for iOS16+
}
} else {
NavigationView {
// navigation view code for iOS15
}
.navigationBarHidden(true)
}
}
in order to hide the UINavigationController
nav bar in iOS15 I needed to hide it using the NavigationView
.
In iOS16 and up I was able to hide it using the UIHostingController
The weirdest part is that I was able to hide it in iOS15 using the UIHostingController
, but only in viewDidAppear
. Nothing happened if I used viewWillAppear
.
Upvotes: 0
Reputation: 1010
You probably will kill me for this, but it is, working lake a charm
override func viewDidLoad() {
super.viewDidLoad()
let hostingVC = UIHostingController(rootView: MySwiftUIView())
let swiftuiView = hostingVC.view!
if #available(iOS 16, *) {
addChild(hostingVC)
} else {
print("Caues memory leak but no double navigation header")
}
view.addSubview(swiftuiView)
hostingVC.didMove(toParent: self)
}
Upvotes: 0
Reputation: 21
I think there is a simpler solution. if you are using Xcode 15 beta, You can rewrite func viewIsAppearing(_ animated: Bool) and implement Func viewIsAppearing (_ Animated: bool) to deal with hiding and appearing in the navigation bar, and it works perfectly.
Upvotes: 2
Reputation: 2487
I used SwiftUI introspect library to hide the extra navigation bar that was only showing for OS version lower than 16.
.introspectNavigationController(customize: { navigationController in
navigationController.navigationBar.isHidden = true
})
Upvotes: 1
Reputation: 2717
This seems fixed on iOS 16: if you add a symbolic breakpoint for -[UINavigationController setNavigationBarHidden:animated:]
and you p $arg3
you'll find that it was nil (false) on iOS 14/15 and it's now 1 (true) on iOS 16, in case you did call setNavigationBarHidden(true
somewhere before this internal call happens, i.e. the internal call doesn't overwrite your code anymore.
On iOS 14, @TParizek's solution works (modifier .navigationBarHidden(true)
), but on iOS 15 I had to call setNavigationBarHidden(true
on the first viewDidLayoutSubviews
call.
Upvotes: 1
Reputation: 836
Hi to all here is my solution how to hide AND BACK navigation bar
import Foundation
import SwiftUI
import UIKit
class HostingController <Content>: UIHostingController<AnyView> where Content : View {
private weak var previousViewController: UIViewController?
private var shouldShowNavigationBar: Bool
private let shouldShowNavigationBarAfterBack: Bool
public init( rootView: Content, previousViewController: UIViewController?,
shouldShowNavigationBar: Bool = false, shouldShowNavigationBarAfterBack: Bool = true) {
self.previousViewController = previousViewController
self.shouldShowNavigationBar = shouldShowNavigationBar
self.shouldShowNavigationBarAfterBack = shouldShowNavigationBarAfterBack
super.init(rootView: AnyView(rootView))
}
override func viewDidLayoutSubviews() {
navigationController?.setNavigationBarHidden(!shouldShowNavigationBar, animated: false)
super.viewDidLayoutSubviews()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf
.previousViewController?
.navigationController?
.setNavigationBarHidden(!strongSelf.shouldShowNavigationBarAfterBack, animated: false)
}
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
How to use it:
let viewController = HostingController(rootView: view, previousViewController: previousViewController)
In case if you need all parameters make not default you can call:
let viewController = HostingController(rootView: view, previousViewController: previousViewController, shouldShowNavigationBar: false, shouldShowNavigationBarAfterBack: false)
previousViewController
- it's a controller that make push of this new one controller.
Upvotes: 0
Reputation: 1922
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.
Upvotes: 26
Reputation: 419
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()
}
Upvotes: 0
Reputation: 121
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
}
}
}
Upvotes: 12
Reputation: 3708
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
}
}
Upvotes: 4
Reputation: 4563
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.
Upvotes: 14
Reputation: 580
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())
Upvotes: 37
Reputation: 331
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
Upvotes: 7
Reputation: 1276
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)
}
Upvotes: 9
Reputation: 195
Do u know where you put in UIKit function inside of swiftUI ?
inside of
var body: some View {
}
you need to call your ViewControllerWrapper class that class need to include some methods in order to use your UIKit class. UIViewControllerRepresentable implementation its also need.
Upvotes: -7