Unable to hide the navigationBar when embedding SwiftUI in UIKit
Asked Answered
O

15

51

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.

Orlan answered 5/11, 2019 at 4:49 Comment(1)
Its works for me - In viewWillAppear if you hide it works but not in viewDidLoadBlackburn
W
37

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())
Wigan answered 11/11, 2020 at 8:17 Comment(3)
I love u man. Hours of work, trying different things on UINavigationBar... and you saved me. Thanks a lot!Tori
.navigationBarHidden(true) works, but the problem is it seems to also disable the "swipe from left side of screen to pop to previous vc" feature. Also, that modifier is deprecated.Sophisticated
great thanks worked for me. I just wanted to hide the back button and keep the navigation bar. so I changed the code as below public init(shouldShowNavigationBar: Bool, rootView: Content) { super.init(rootView: AnyView(rootView.navigationBarBackButtonHidden(!shouldShowNavigationBar))) }Modie
V
26

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.

Vincenza answered 15/2, 2022 at 17:58 Comment(3)
This doesn't actually work on iOS 16 if you want to have nav bar items in your SwiftUI view - doing this apparently hides the SwiftUI nav bar as well, not just the UIKit one.Straub
@DávidPásztor that's correct. This forces the navigation bar to be hidden – booth the one defined in UIKit and the one defined in SwiftUI. Hard to know for sure, but I think UIKit and SwiftUI uses the same navigation bar instance under the hood.Vincenza
Worked great under iOS14, thanks.Residuary
Q
14

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.

Quartzite answered 8/1, 2021 at 9:10 Comment(0)
F
11

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
    }
  }
}
Found answered 21/10, 2021 at 5:19 Comment(2)
I had a UIHostingController as a child of a UIViewController subclass, and I was hiding the navigation bar in the parent view controller's viewWillAppear, which was not working for the hosted SwiftUI. But moving my hiding to viewWillLayoutSubviews as you suggested her fixed it for me, thanks!Fatherland
…except that the nav bar still seems to be unhiding when you pop back to that view controller smhFatherland
S
9

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)
}
Schroth answered 9/5, 2020 at 10:16 Comment(1)
For me, this caused the view to jump. Putting it in viewDidLayoutSubviews works without the jumpHindi
R
7

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
Resurgent answered 2/9, 2020 at 15:56 Comment(0)
A
4

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
        }
    }
Anterior answered 14/4, 2021 at 14:58 Comment(0)
G
1

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.

Gloxinia answered 5/12, 2022 at 15:27 Comment(0)
I
1

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
            })
Iraqi answered 13/12, 2022 at 0:5 Comment(0)
B
1

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.

Bernadettebernadina answered 12/9, 2023 at 4:26 Comment(0)
B
0

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()
}
Byron answered 26/11, 2021 at 13:22 Comment(0)
S
0

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.

Slogan answered 12/10, 2022 at 4:9 Comment(0)
G
0

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)
}
Gabor answered 19/1, 2024 at 3:57 Comment(0)
D
0

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.

Dicho answered 13/5, 2024 at 19:17 Comment(0)
M
-7

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.

Martin answered 5/11, 2019 at 7:34 Comment(3)
I think you have my issue reversed. I am trying to embed SwiftUI --> UIKit.Orlan
look at this maybe it will help you to find what you searchMartin
my problem is not one of adding the swiftUI to the view. But rather the fact that it carries with it an immoveable navigationBar.Orlan

© 2022 - 2025 — McMap. All rights reserved.