UISheetPresentationController Underneath Tab Bar

1k views Asked by At

I am trying to present a UIViewController within a UISheetPresentationController to have a permanent modal that sits below my UITabBarController exactly like how Apple has shown it possible in the "Find My" app:

UISheetPresentationController Underneath Tab Bar

Reference Code:

let navigationController = UINavigationController(rootViewController: UIViewController());

navigationController.modalPresentationStyle = .formSheet;
if let sheet = navigationController.sheetPresentationController {
    sheet.detents = [.medium(), .large()];
    sheet.prefersGrabberVisible = true;
    sheet.largestUndimmedDetentIdentifier = .medium;
    sheet.prefersScrollingExpandsWhenScrolledToEdge = false;
}

present(navigationController, animated: true);

This post: UISheetPresentationController with a tabBar poses a similar question but does not have any answers.

1

There are 1 answers

3
HangarRash On

There are several aspects to this issue and its solution.

To start, we need a custom UISheetPresentationController to prevent the presented view controller from covering the tab bar. The following class can be used:

class TabSheetPresentationController : UISheetPresentationController {
    override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()

        // Update the container frame if there is a tab bar
        if let tc = presentingViewController as? UITabBarController, let cv = containerView {
            cv.clipsToBounds = true // ensure tab bar isn't covered
            var frame = cv.frame
            frame.size.height -= tc.tabBar.frame.height
            cv.frame = frame
        }
    }
}

The above class shortens the height of the container view so the tab bar is exposed allowing user interaction with the tab bar even while the presented sheet is in view. It also ensures that if the user pulls down the presenting view, the bottom doesn't cover the tab bar.

In order to make use of the custom presentation controller, we can no longer use the standard sheetPresentationController property of the view controller to be presented. Instead, we need to provide a custom modal presentation.

First, the code to create and present the view controller to be shown in the sheet would be something like the following:

let vc = UIViewController()
let nc = UINavigationController(rootViewController: vc)
nc.modalPresentationStyle = .custom
nc.transitioningDelegate = self
nc.isModalInPresentation = true // don't let it be dismissed by dragging to the bottom

present(nc, animated: false)

Note the line nc.transitioningDelegate = self. This requires the class represented by self to conform to the UIViewControllerTransitioningDelegate protocol.

Let's say self is a SomeTabViewController class representing one of the tabs in the view controller. We can then add:

extension SomeTabViewController: UIViewControllerTransitioningDelegate {
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        let sc = TabSheetPresentationController(presentedViewController: presented, presenting: source)
        sc.detents = [
            .mySmall(),
            .medium(),
            .myLarge(),
        ]
        sc.largestUndimmedDetentIdentifier = .myLarge
        sc.prefersGrabberVisible = true
        sc.prefersScrollingExpandsWhenScrolledToEdge = false
        sc.widthFollowsPreferredContentSizeWhenEdgeAttached = true
        sc.selectedDetentIdentifier = .medium

        return sc
    }
}

That code basically replaces the usual setup code used with the presented view controller's sheetPresentationController property.

In the example code I'm making use of two custom detents. Those are provided with the following:

extension UISheetPresentationController.Detent.Identifier {
    static let mySmall = UISheetPresentationController.Detent.Identifier("mySmall")
    static let myLarge = UISheetPresentationController.Detent.Identifier("myLarge")
}

extension UISheetPresentationController.Detent {
    class func mySmall() -> UISheetPresentationController.Detent {
        return UISheetPresentationController.Detent.custom(identifier: .mySmall) { context in
            return 60
        }
    }

    class func myLarge() -> UISheetPresentationController.Detent {
        return UISheetPresentationController.Detent.custom(identifier: .myLarge) { context in
            return context.maximumDetentValue - 0.1
        }
    }
}

The small dentent lets the user minimize the sheet and the custom large detent is a trick that allows the sheet to be shown nearly fullscreen without the side effect of the underlying view controller shrinking like you normally get with the standard .large() detent.

Note that the custom sheet presentation has only been tested on an iPhone and in portrait. Further work is likely needed to fully support an iPhone in landscape and the likely need for different layout on an iPad. I leave that as an exercise for the reader.

The above code basically answers the question of showing a presented view controller in a sheet presentation while still allowing the tab bar to be visible and active.

However, this brings up the next big issue. When you present a view controller from one of the tab bar controller's view controllers, the presented view controller is actually presented from the tab bar controller, not the original view controller. This means that only one view controller (tab) of the tab bar controller can present a sheet at any one time. Using the code I've provided above, as you switch tabs, the sheet stays in view. If this is not desired then logic needs to be added to dismiss the sheet when a different tab is selected.

The "Find My" app shows a sheet on all four tabs. I strongly believe that there is only one sheet being shown from the tab bar controller. Its contents are updated based on whichever tab is currently selected.

Given this, and depending on your own requirements, you may need to change my solution just a bit so the the sheet's view controller is presented directly by the tab bar controller and not from one of the tab view controllers. Handling the update of the content based on the selected tab is beyond the original scope and I leave that details as an exercise for the reader.