Mike Perhats
Mike Perhats

Reputation: 673

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.

Upvotes: 53

Views: 21626

Answers (15)

Christopher Larsen
Christopher Larsen

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

Ivan Besarab
Ivan Besarab

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

coderGuo
coderGuo

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

Ali
Ali

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

Gobe
Gobe

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

Max Tymchii
Max Tymchii

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

Gustaf Rosenblad
Gustaf Rosenblad

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

Elvis Rudonja
Elvis Rudonja

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

getogrand
getogrand

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

Amir Khorsandi
Amir Khorsandi

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

Frederik Winkelsdorf
Frederik Winkelsdorf

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

TParizek
TParizek

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

Wilson Mu&#241;oz
Wilson Mu&#241;oz

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

Mika
Mika

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

Bartu Akman
Bartu Akman

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

Related Questions