mapbox-maps-ios
mapbox-maps-ios copied to clipboard
Attribution Button fails to present when using presentation detents
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 })
}
}
Thank you for bringing this to our attention, I have brought this to the larger team for investigation. In the meantime, you can modify the responder chain so that the closest responder is the view controller that you want to present.
Hi there, I'm experiencing something similar with the Swift UI implementation as well when attempting to tap on the Attribution Button when a sheet is opened. Was wondering if there is a workaround for this.
Code
import SwiftUI
@_spi(Experimental) import MapboxMaps
struct ContentView: View {
@State private var settingsDetent: PresentationDetent = PresentationDetent.fraction(0.1)
@State private var showSheet = true
var body: some View {
Map()
.mapStyle(.standard)
.ignoresSafeArea()
.sheet(isPresented: $showSheet) {
Text("Hello world")
.interactiveDismissDisabled()
.presentationDetents(
Set([
PresentationDetent.fraction(0.01),
PresentationDetent.fraction(0.5)
]),
selection: $settingsDetent
)
.presentationBackgroundInteraction(.enabled)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Error:
Attempt to present <UIAlertController: 0x106a5fc00> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x107810a00> (from <_TtCV10MapboxMaps3MapP33_54ECFE62197C71526B43BC8003CCBCE817MapViewController: 0x105a16080>) which is already presenting <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x106074800>.