ToolbarItemGroup With Palette Style Cannot Present a View Controller While the Context Menu Is Visible

When I set up a toolbar item group with multiple options and set the controlGroupStyle as .palette, and when one of the options are supposed to present a view controller, I get the following error

Attempt to present <UINavigationController: 0x101813200> on <ProjectName.HomeTabBarViewController: 0x10701bc00> (from <UINavigationController: 0x107821000>) which is already presenting <_UIContextMenuActionsOnlyViewController: 0x1035c19d0>.

So basically the context menu we see is under the hood a view controller that is being presented.

Is there a right way of fixing it, or is it maybe something that will be fixed by Apple?

This is how I set up the ToolbarItemGroup on SwiftUI and the view model ultimately presents another UINavigationController that has a UIHostingController as its view controller:

.toolbar {
            ToolbarItemGroup(placement: .topBarTrailing) {
                Button("ft_commons_edit".localised, systemImage: "pencil") {
                    viewModel.didTapEditAction()
                }
                Button("ft_commons_delete".localised, systemImage: "trash") {
                    viewModel.didTapDeleteAction()
                }
            } label: {
                Image("edit-icon")
                    .resizable()
                    .frame(width: 24.0, height: 24.0)
            }
        }
        .controlGroupStyle(.palette)

This is how the view looks like when presentation fails:

As a workaround, I found two options that work well. I’d like to share them and ask for recommendations, just to make sure they won’t cause any unexpected issues later on:

Option 1: Access the top most visible view controller and attempt presenting on it

This requires the following extension:

extension UIViewController {
    func topMostViewController() -> UIViewController {
        if let presentedViewController = self.presentedViewController {
            return presentedViewController.topMostViewController()
        } else if let navigationController = self as? UINavigationController,
                  let topViewController = navigationController.topViewController {
            return topViewController.topMostViewController()
        } else if let tabBarController = self as? UITabBarController,
                  let selectedViewController = tabBarController.selectedViewController {
            return selectedViewController.topMostViewController()
        } else {
            return self
        }
    }
}

Then called as:

navigationController.topMostViewController().present(exerciseEditingNavController, animated: true)

Option 2: Call dismiss before attempting to present anything

This also works fine, even without any delay, the menu is first dismissed and presentation works fine afterwards, code example:

navigationController.dismiss(animated: true)
navigationController.present(exerciseEditingNavController, animated: true)

I feel like Option 1 would be the better choice, because if this context menu is ever no longer treated as a view controller and direct presentation starts working, Option 1 would still behave correctly by presenting from the topmost visible view controller. Option 2, on the other hand, could introduce a bug by dismissing an unrelated view controller if the context menu is no longer represented as a view controller at that point.

I would appreciate any advice from anyone who has experienced this, or from Apple developers.

Thanks

Thanks for the post, and great UI in my opinion. Hope as many SwiftUI people jump into this thread but I think the issue is because you are using UIKit.

I think based on the error message, the error you are seeing is a UIKit presentation issue when a UI isn’t finished so looks like .palette control group style on iOS is backed by UIKit's context menu system (UIContextMenuInteraction). https://developer.apple.com/documentation/uikit/uicontextmenuinteraction

When the menu is open, a private view controller (_UIContextMenuActionsOnlyViewController) is actively being presented.

I think the issue here is as UIKit strictly enforces that a UIViewController can only present one view controller at a time, but you call to present(...) fails because the underlying navigation controller is still busy presenting the context menu? Is it timing the issue?

If possible, you should avoid calling UIKit's present() manually from a SwiftUI button action. Instead, update a @State or @Published property in your view model, and let SwiftUI handle the presentation via .sheet or .fullScreenCover? As SwiftUI's internal engine is smart enough to queue presentations and wait for the context menu to dismiss before presenting the sheet.

Albert
  Worldwide Developer Relations.

ToolbarItemGroup With Palette Style Cannot Present a View Controller While the Context Menu Is Visible
 
 
Q