Search code examples
swiftuiviewcontrollerpopupstoryboarduitabcontroller

How to change an existing UIViewController (which is controlled by a UITabController) so it will appear as a pop up?


Apologies, Swift newbie here. I know there are other answers to questions "similar" to this, but I believe this scenario is different:

I am making some UI changes to an existing app. As you can see from the included image, the UI was created from the storyboard. There is a Main Tab Controller that controls 5 UIViewControllers. They all work fine, but I need VC4 to open as a pop up on top of whichever other VC is displayed at the time. I've tried setting different Transition Style and Presentation settings but they don't seem to change anything.

I'd like it to slide up from the bottom. Also (and it's not shown in this example image) there would be a smaller View in the center of VC4 which will be a dialog that would need to be dismissed before anything else (including other Tab Bar items) could happen. Ideally, the area around this smaller view would be a dimmed version of whichever VC is underneath.

How do I accomplish this?

enter image description here


Solution

  • The following solution involves creating a custom UITabBarController subclass. This custom class saves off the controller's last view controller and replaces it with an empty view controller. Then it intercepts a check if a tab should be selected. If the last tab is being selected then the selection is rejected and then the last view controller is presented modally.

    class SpecialTabBarController: UITabBarController {
        private var lastViewController: UIViewController?
    
        // When the view controllers are set, replace the last view controller
        // with a dummy controller (so the tab appears normally). Save off the
        // the real last view controller for modal presentation when needed.
        override var viewControllers: [UIViewController]? {
            didSet {
                if let viewControllers, viewControllers.count > 1 {
                    lastViewController = viewControllers.last
                    var newVCs = Array(viewControllers.dropLast())
                    let extra = UIViewController()
                    extra.tabBarItem = lastViewController?.tabBarItem
                    newVCs.append(extra)
                    super.viewControllers = newVCs
                }
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            delegate = self
    
            if let vcs = viewControllers {
                viewControllers = vcs
            }
        }
    }
    
    extension SpecialTabBarController: UITabBarControllerDelegate {
        // When a tab is tapped, this is called to confirm whether the
        // corresponding controller should be shown. Here we disable the
        // showing of the last view controller and then manually present it.
        func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
            if let lastViewController, viewController === viewControllers?.last {
                present(lastViewController, animated: true)
                return false
            } else {
                return true
            }
        }
    }
    

    This solution will work with a programmatically created tab bar controller as well as with one created in a storyboard. To work in a storyboard, update the tab bar controller's class from the default of UITabBarController to SpecialTabBarController. That's the only change to make this work.

    This solution does cause one limitation. Since SpecialTabBarController makes itself its own UITabBarControllerDelegate, you can't make any other object the delegate. Though there are solutions to this if needed. That's left as an exercise for the reader.

    For the dialog you want in the center of the last view controller (VC4), you can have the code for that view controller display a UIAlertController when it appears.