mapbox-maps-ios icon indicating copy to clipboard operation
mapbox-maps-ios copied to clipboard

Attribution Button fails to present when using presentation detents

Open vdka opened this issue 2 years ago • 2 comments

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

IMG_E3DA2AE1E8F4-1

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 })
    }
}

vdka avatar Dec 13 '22 00:12 vdka

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.

ZiZasaurus avatar Dec 14 '22 02:12 ZiZasaurus

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>.

blahblahblah- avatar Dec 09 '23 18:12 blahblahblah-