Unable to hide the navigationBar when embedding SwiftUI in UIKit - swift

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.

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())

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.

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.

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)
}

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

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

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

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.

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()
}

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.

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
})

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.

Related

How to present a ViewController as a Modal Sheet without the background shadow

Is there any way to present a ViewController as a Modal Sheet without the background shadow as shown in the first image below using swift. Is there an easy way or should we need to write custom UIPresentationController? [![The required output][1]][1]
[1]: https://i.stack.imgur.com/QAEEn.png![enter image description here](https://i.stack.imgur.com/q4JD5.jpg)
You can use a smaller size view controller as per your need. Firstly add a class.
class SmallSizePresentationController : UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
get {
guard let theView = containerView else {
return CGRect.zero
}
return CGRect(x: 0, y: theView.bounds.height * (281 / 896), width: theView.bounds.width, height: theView.bounds.height)
}
}
}
Then when you want to present this type of view controller just add the extension of your current view controller.
extension YourCurrentViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return SmallSizePresentationController(presentedViewController: presented, presenting: presenting)
}
}
Then present your new view controller like this
let VC = withoutShadowVC()
VC.transitioningDelegate = self
VC.modalPresentationStyle = .custom
self.present(VC, animated: true, completion: nil)
You can modify the view controller height.

Presenting UISearchController programmatically

I am presenting a UISearchController programmatically, without adding it to the navigationItem. The Calendar app does something similar.
Without a presentation context, the search bar appears correctly, but persists after pushing another view controller.
This is expected, so we need to set definesPresentationContext on the list view controller... But that causes the search bar to render incorrectly.
Here's the code for context:
private lazy var searchController: UISearchController! = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
// If this is set to true, the search bar animates correctly, but that's
// not the effect I'm after. See the next video.
searchController.hidesNavigationBarDuringPresentation = false
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true
searchButton.rx.tap.subscribe(onNext: { [unowned self] in
present(searchController, animated: true)
}).disposed(by: disposeBag)
}
Setting hidesNavigationBarDuringPresentation kind of fixes it, but we lose the tab bar, and the whole thing just looks bad.
I tried this solution (Unable to present a UISearchController), but it didn't help.
Any suggestions?
UPDATE: The issue is, more specifically, that the search bar appears behind the translucent navigation bar. Making the nav bar solid ( navigationController?.navigationBar.isTranslucent = false) makes the search bar appear under the nav bar.
I have the same problem not been able to solve this either. It seems like the problem is that either
a) the searchcontroller is presented at the very top of the viewcontroller stack, even above the navigation controller, so that it stays active into the next viewcontroller push. or,
b) the searchcontroller is presented underneath the navigationcontroller so that it remains covered by the navigation bar
One idea: don't embed the viewcontroller which is presenting the searchcontroller in a navigation controller. instead, just create a UIView which looks like a navigation bar a the top. would this be an inappropriate solution?
I haven't found a solution to the original problem, but I found a workaround: intercept navigation events, and manually dismiss the search controller.
override func viewDidLoad() {
...
// This makes the search bar appear behind the nav bar
// definesPresentationContext = true
navigationController?.delegate = self
}
extension JobListViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController, animated: Bool) {
// `animated` is false because, for some reason, the dismissal animation doesn't start
// until the transition has completed, when we've already arrived at the new controller
searchController.dismiss(animated: false, completion: nil)
}
}
I found that presenting the search controller over the navigation bar can be achieved by calling present(_:animated:completion:) on the navigation controller itself rather than the navigation controller's child.
So in your view controller you can do
navigationController?.present(searchController, animated: true)
And this will behave like the search button in the Apple's Calendar app.
Update
Regarding dismissing the search controller before pushing a new controller to the navigation stack, you can do this manually depending on how the push is done.
All the bellow will animate dismissing the search controller before the push happens. Note that I disable user interaction until the dismiss animation completes to prevent pushing the same view controller multiple times.
UIStoryboardSegue
Add this override to your view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if navigationController?.presentedViewController == searchController {
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
}
}
IBAction (Programmatically)
Just dismiss the search controller before pushing view controllers to the navigation stack:
#IBAction func showSecondTapped(_ sender: UIButton) {
// Dismiss the search controller first.
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
// Build and push the detail view controller.
if let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") {
navigationController?.pushViewController(secondViewController, animated: true)
}
}
Handling pop gesture
If the view controller that is presenting the search controller is not the root of your navigation controller, the user might be able to use the interactive pop gesture, which will also keep the search controller presented after the pop. You can handle this by making your view controller the delegate for the search controller, conform to UISearchControllerDelegate and adding the following code:
extension ViewController: UISearchControllerDelegate {
func willPresentSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
func willDismissSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
}

Swift, newbie question related to containerView. how to make Storyboard viewcontroller for child the same size as a container view

I've been trying to correctly implement a ContainerView in my practice app after watching and reading various tutorials. I initially tried creating the child by utilizing the storyboard and dragging the ContainerView onto the ViewController in question, and then utilizing the child ViewController that is then automatically created, but I need to have multiple child ViewControllers and I couldn't quite figure that out. I researched some programatic ways to do it and I successfully have it functioning the way I want. The only hiccup being that when I am viewing the child ViewControllers on my storyboard they are full size and do not correlate in size to my ContainerView. So I have to have a bit of trial and error in getting the objects I place in the child to fit in the ContainerView.
Can anyone give me some pointers on how I fix that? I've used the code below. The function runs when a button on the parent ViewController is touched. There are other associated child ViewControllers: child2, child3 that run depending on which button is pushed. I didn't include that extra code below for the sake of being concise.
private lazy var child1: PersonInfoChildView1Controller = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
var viewController = storyboard.instantiateViewController(withIdentifier: "Child1VC") as! PersonInfoChildView1Controller
viewController.person = self.person
addChild(viewController)
return viewController
}()
//MARK: ADD THE CHILD
private func add(asChildViewController viewController: UIViewController) {
containerView.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = containerView.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParent: self)
}
Try to set your child view controller constraints relative to the container view.
Edit your add method like this:
private func add(asChildViewController viewController: UIViewController)
{
viewController.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
viewController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
viewController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
// Notify Child View Controller
viewController.didMove(toParent: self)
}
Also you can create an extension to UIView class to pin the view to the parent view by just one line code
extension UIView {
func pin(to superview: UIView){
translatesAutoresizingMaskIntoConstraints = false
topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
}
}
put this in a separate swift file with this method you can make the container view same size as the view controller
containerView.addSubview(viewController.view)
viewController.view.pin(containerView)
This will save you a lot of time in the future.

Present below current view and not above

I'm trying to present a view controller below another presented view controller (like WhatsApp when you open camera and press gallery).
I tried many things but none worked ..
Use child view controller and set view of that added child view controller at the top of hierarchy. It will be top most element, so actual background of this view will be obscured, but that's the way to go.
//code inside UIViewController class
func addViewControllerAtBottom() {
let newVC = NewVCType() //just instantiate it
addChildViewController(newVC)
view.insertSubview(newVC.view, at: 0) //at 0 means it's first rendered, all others will be on top of it
}
You can reproduce this behavior by doing the following :
First, create a NavigationController with a root ViewController :
let navController = UINavigationController(rootViewController: firstController)
Then, present this navigationController with animated: false and in the completion of the present method, push your second ViewController, still with animated: false (to avoid weird animations) :
present(navController, animated: false) {
navController.pushViewController(secondController, animated: false)
}
Here you go, you got a new navigation with 2 UIViewController, like WhatsApp.
Full code, wrapped into a button's action :
#IBAction func buttonTapped(_ sender: Any) {
let navController = UINavigationController(rootViewController: firstController)
present(navController, animated: false) {
navController.pushViewController(secondController, animated: false)
}
}

Animation segue bug on the navigation bar with large title

My bug:
If navigate from a view controller with large titles enabled to a view controller with large titles disabled i see same bug. Height navigation bar changes not smoothy.
I want animation change height navBar during segue on another viewController like this
Common propertyes for navBar set up in BaseNavigationController
class BaseNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
setNavBarTitlesPropertyes()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
private func setNavBarTitlesPropertyes() {
navigationBar.tintColor = .white
navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.white
]
if #available(iOS 11.0, *) {
navigationBar.prefersLargeTitles = true
navigationBar.largeTitleTextAttributes = [
.foregroundColor: UIColor.white
]
}
}
And my setting navbar in the storyboard:
I found solution for this trouble. UINavigationBar
property translucent should be true, and also bottom and top constraint for tableView in UIViewController should be equal Superview.Top and Superview.Bottom accordingly.