mapbox / mapbox/mapbox-maps-ios

Attribution Button fails to present when using presentation detents

Open
#1,799 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

auto-triaged bug :beetle:
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

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.