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.