mapbox / mapbox/mapbox-maps-ios
Attribution Button fails to present when using presentation detents
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 601
- Forks
- 196
- PR merge metrics
- No merged PRs in 30d
Description
Environment
- Xcode version: 14
- iOS version: 16
- Devices affected: all
- Maps SDK Version: 10.10.0
Observed behavior and steps to reproduce
When you have a sheet presented over the top of a Map View the attribution button fails to present an action sheet because the map views parent controller is already presenting.
Screenshot

Error
[Presentation] Attempt to present <UIAlertController: 0x139822600> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x138819c00> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x138819c00>) which is already presenting <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGVS_10_ShapeViewVS_9RectangleVS_8Material_VS_30_SafeAreaRegionsIgnoringLayout__: 0x13b01d800>.
Expected behavior
The attribution action sheet should present successfully, in the foreground of the presented sheet.
Notes / preliminary analysis
The button attempts to present using:
func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController {
return parentViewController!
}
In this specific case the presentation would work if viewControllerForPresenting walked the view controller hierarchy until it finds the 'topViewController' and uses this for presentation instead.
Reproduction
Code
import SwiftUI
import MapboxMaps
let smallSheetHeight = 100 as CGFloat
struct ContentView: View {
var body: some View {
MapView()
.ignoresSafeArea()
.background {
BottomSheetPresenter {
Rectangle().fill(.regularMaterial).ignoresSafeArea()
}
}
}
}
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let resourceOptions = ResourceOptions(accessToken: "")
let cameraOptions = CameraOptions(zoom: 1)
let mapInitOptions = MapInitOptions(resourceOptions: resourceOptions, cameraOptions: cameraOptions)
let mapView = MapboxMaps.MapView(frame: .zero, mapInitOptions: mapInitOptions)
mapView.ornaments.options.logo.margins.y = smallSheetHeight + 8
mapView.ornaments.options.attributionButton.margins.y = smallSheetHeight + 8
return mapView
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
}
struct BottomSheetPresenter<Content>: UIViewControllerRepresentable where Content: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> SheetPresentingController {
return SheetPresentingController(content: content)
}
func updateUIViewController(_ viewController: SheetPresentingController, context: Context) {
viewController.contentController.rootView = content
}
final class SheetPresentingController: UIViewController, UISheetPresentationControllerDelegate {
let contentController: UIHostingController<Content>
init(content: Content) {
self.contentController = UIHostingController(rootView: content)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
guard parent != nil else { return }
if presentedViewController == nil {
presentSheet()
}
}
func presentSheet() {
contentController.view.backgroundColor = nil
contentController.modalPresentationStyle = .pageSheet
contentController.presentationController?.delegate = self
// Disable swipe to dismiss
contentController.isModalInPresentation = true
guard let sheet = contentController.sheetPresentationController else {
fatalError("`sheetPresentationController` should be non-nil given `modalPresentationStyle` is `pageSheet`")
}
sheet.detents = [.small(), .medium(), .large()]
sheet.largestUndimmedDetentIdentifier = .medium
sheet.prefersGrabberVisible = true
sheet.prefersScrollingExpandsWhenScrolledToEdge = true
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
sheet.selectedDetentIdentifier = .small
self.present(contentController, animated: true)
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
}
}
extension UISheetPresentationController.Detent.Identifier {
static let small = UISheetPresentationController.Detent.Identifier(rawValue: "small")
}
extension UISheetPresentationController.Detent {
static func small() -> UISheetPresentationController.Detent {
UISheetPresentationController.Detent.custom(identifier: .small, resolver: { _ in smallSheetHeight })
}
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating viewControllerForPresenting(_:) and inspect how AttributionDialogManager chooses its presenter when a sheet is already displayed. Reproduce the SwiftUI presentation-detents example, then verify that the attribution action sheet appears in front of the presented sheet without a presentation warning.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, swift
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100