Search code examples
iosswiftuinavigationcontrolleruitabbarcontrollerswift5

Swift 5 | UINavigationController not appearing until going to DetailsViewController


I'm working on rebuilding my app programmatically instead of using storyboards and running into issues with my UINavigationController. When the app first loads the root ViewController, the bar containing "carrier" and the time is a light version of the color that I've assigned and the search bar exists instead of a normal navigation bar.

When I tap/click a cell on my UITableView to get to the details screen then go back, my UINavigationBar appears correctly but the search bar disappears. I'm sure I've confused something but I'm not sure where I messed up or if I am missing something entirely. Any and all help would be greatly appreciated. I've also included an image as an example of whats going on.

Nonexistant UINavigationController > Detail screen > Back.gif

I have setup a TabBarController in the SceneDelegate SceneDelegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: - Properties
var window: UIWindow?
let tabBarDelegate = TabBarDelegate()
let userAuthToken = UserDefaults.standard.string(forKey: "token")
let userKeyToken = UserDefaults.standard.string(forKey: "key")


func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    // Disable dark mode
    if #available(iOS 13.0, *) {
        window?.overrideUserInterfaceStyle = .light
    }

    // IF USER IS LOGGED IN
    if let windowScene = (scene as? UIWindowScene) {

        if let _ = userAuthToken {
            self.window = UIWindow(windowScene: windowScene)

            // CREATE TAB BAR //
            let tabController = UITabBarController()

            tabController.tabBar.backgroundColor = .white

            // Instantiate the storyboards
            let cannabisStoryboard = UIStoryboard(name: "Cannabis", bundle: nil)
            let profileStoryboard = UIStoryboard(name: "Profile", bundle: nil)


            // Instantiate the view controllers to storyboards
            let cannabisVC = cannabisStoryboard.instantiateViewController(withIdentifier: "Cannabis") as! CannabisViewController
            let profileVC = profileStoryboard.instantiateViewController(withIdentifier: "Profile") as! ProfileViewController

            // Displays the items in below order in tab bar
            let vcData: [(UIViewController, UIImage, UIImage)] = [
                (cannabisVC, UIImage(named: "Cannabis_icon")!, UIImage(named: "Cannabis_icon_selected")!),
                (profileVC, UIImage(named: "Profile_icon")!, UIImage(named: "Profile_icon_selected")!),
            ]

            let vcs = vcData.map { (vc, defaultImage, selectedImage) -> UINavigationController in
                let nav = UINavigationController(rootViewController: vc)
                nav.tabBarItem.image = defaultImage
                nav.tabBarItem.selectedImage = selectedImage

                return nav
            }

            // Assign to tab bar controller
            tabController.viewControllers = vcs
            tabController.tabBar.isTranslucent = false
            tabController.delegate = tabBarDelegate

            // Disables rendering for tab bar images
            if let items = tabController.tabBar.items {
                for item in items {
                    if let image = item.image {
                        item.image = image.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
                    }

                    if let selectedImage = item.selectedImage {
                        item.selectedImage = selectedImage.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
                    }

                    // Hides title
                    item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
                }
            }

            // Customize Navigation bar
            UINavigationBar.appearance().backgroundColor = UIColor(rgb: 0x00ffcc)

            // Disable dark mode
            window!.overrideUserInterfaceStyle = .light

            window?.rootViewController = tabController
            window?.makeKeyAndVisible()



        } else {
            // let loginStoryboard = UIStoryboard(name: "Login", bundle: nil)
            // let loginViewController = loginStoryboard.instantiateViewController(withIdentifier: "Login") as! LoginViewController

            // Disable dark mode
            window!.overrideUserInterfaceStyle = .light

            // window?.rootViewController = loginViewController
            window?.rootViewController = LoginViewController()
            window?.makeKeyAndVisible()
        }



        window?.windowScene = windowScene
    }

}

Root ViewController (stripped down to relevant info)

class CannabisViewController: UIViewController {

// MARK:- Outlets
let tableView = UITableView()

// MARK:- Properties
var cannabisDetailViewController: CannabisDetailsViewController? = nil


// Search
let searchController = UISearchController(searchResultsController: nil)
var isSearchBarEmpty: Bool { return searchController.searchBar.text?.isEmpty ?? true }
var filteredStrains = [Cannabis]()
var isFiltering: Bool { return searchController.isActive && !isSearchBarEmpty }


// MARK: - ViewWillLayoutSubViews
override func viewWillLayoutSubviews() {
    let navigationBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 44))

    // navigationController?.setViewControllers([CannabisViewController()], animated: true)


    self.view.addSubview(navigationBar)
}

// MARK: - ViewDidLoad
override func viewDidLoad() {
    super.viewDidLoad()

    configurePage()


    // MARK: Self sizing table view cell
    tableView.estimatedRowHeight = CGFloat(88.0)
    tableView.rowHeight = UITableView.automaticDimension



    // MARK: DataSource/Delegate
    tableView.dataSource = self
    tableView.delegate = self


    // Removes default lines from table views
    tableView.tableFooterView = UIView()
    tableView.separatorStyle = .none


    // MARK: Navigation: logo in center
    let logoHeader = UIImageView(image: UIImage(named: "logoHeader"))
    self.navigationItem.titleView = logoHeader


    // MARK: API
    getCannabisList()


    // MARK: Search bar controller
    searchController.searchResultsUpdater = self
    searchController.obscuresBackgroundDuringPresentation = false
    searchController.searchBar.placeholder = "Search for a strain!"
    navigationItem.searchController = searchController
    definesPresentationContext = true

}


// Configure TableView
func configurePage() {
    // Configure Tableview
    view.addSubview(tableView)
    tableView.anchor(top: view.topAnchor, left: view.leftAnchor,
                     bottom: view.bottomAnchor, right: view.rightAnchor)
}

}

SearchController (Still in the root ViewController)

    func updateSearchResults(for searchController: UISearchController) {
    let searchBar = searchController.searchBar
    let userSearch = searchBar.text!.trimmingCharacters(in: .whitespaces)
    search(searchText: userSearch)
}

Solution

  • I can see a couple of issues here, but first to initialize your window in the SceneDelegate you would use the UIWindowScene:

    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: windowScene)
        self.window = window
        let rootViewController = RootViewController()
        window.rootViewController = UINavigationController(rootViewController: rootViewController)
        window.makeKeyAndVisible()
    }
    

    If you want to disable Dark Mode globally in your app, simply add a key UIUserInterfaceStyle to your Info.plist and set its value to Dark (or Light). By doing this, you won't need to update each view controller because it will overwrite the global app default style. I highly encourage you to add support for Dark Mode though!

    If you want to change your navigation bar appearance:

    if #available(iOS 13.0, *) {
        let navigationAppearance = UINavigationBarAppearance()
        navigationAppearance.configureWithOpaqueBackground()
        navigationAppearance.backgroundColor = .white
        navigationAppearance.titleTextAttributes = // ...
        navigationAppearance.largeTitleTextAttributes = // ...
        UINavigationBar.appearance().tintColor = .systemBlue
        UINavigationBar.appearance().barTintColor = .white
        UINavigationBar.appearance().standardAppearance = navigationAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance
    } else {
        UINavigationBar.appearance().backgroundColor = .white
        UINavigationBar.appearance().barTintColor = .white
        UINavigationBar.appearance().tintColor = .systemBlue
        UINavigationBar.appearance().titleTextAttributes = // ...
        UINavigationBar.appearance().largeTitleTextAttributes = // ...
    }
    

    I made a minimal project for you to see a working example with the search bar, in which the flickering of the status bar doesn't happen, and the UISearchBar's hide/show animation works properly when pushing/popping your DetailViewController:

    RootViewController:

    import UIKit
    
    class RootViewController: UIViewController {
    
        private let reuseIdentifier = "reuseIdentifier"
    
        lazy var tableView: UITableView = {
            $0.delegate = self
            $0.dataSource = self
            $0.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
            return $0
        }(UITableView(frame: .zero, style: .grouped))
    
        private lazy var searchController: UISearchController = {
            $0.searchResultsUpdater = self
            $0.delegate = self
            $0.searchBar.delegate = self
            $0.obscuresBackgroundDuringPresentation = false
            $0.hidesNavigationBarDuringPresentation = false
            $0.searchBar.backgroundColor = .white
            $0.searchBar.tintColor = .systemBlue
            return $0
        }(UISearchController(searchResultsController: nil))
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupViews()
            setupConstraints()
        }
    
        func setupViews() {
            title = "Source"
            view.backgroundColor = .white
            navigationItem.searchController = searchController
            navigationItem.hidesSearchBarWhenScrolling = false
            view.addSubview(tableView)
            // ...
        }
    
        func setupConstraints() {
            tableView.translatesAutoresizingMaskIntoConstraints = false
            tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        }
    }
    
    extension RootViewController: UITableViewDelegate {
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let detailViewController = DetailViewController()
            navigationController?.pushViewController(detailViewController, animated: true)
        }
    
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            return nil
        }
    
        func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
            return CGFloat.leastNonzeroMagnitude
        }
    }
    
    extension RootViewController: UITableViewDataSource {
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 10
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
            cell.textLabel?.text = "Cell at indexPath \(indexPath)"
            return cell
        }
    }
    
    extension RootViewController: UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {
        func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {}
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {}
        func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {}
        func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {}
        func updateSearchResults(for searchController: UISearchController) {}
    }
    

    DetailViewController:

    class DetailViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            title = "Detail"
            view.backgroundColor = .systemGray6
        }
    }