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

Answered by DTS Engineer in 886379022

Thanks for your post and answering my questions as well as providing more information that I didn’t had before. More and more looks like a UIKit timing issue, I know you do not think so, but give me a chance. When I look at the error is the common error when a View has not finished and I try to load a new one. Wrapping SwiftUI views in UIHostingController and managing the navigation stack via UINavigationController is the standard migration path for established UIKit apps. SwiftUI is designed to interoperate smoothly with this architecture, so you shouldn't be forced to migrate your entire presentation layer to SwiftUI just to get a context menu to dismiss properly.

The behavior you are describing—where a menu presented from a ToolbarItemGroup refuses to dismiss upon selection unless a SwiftUI-native presentation modifier (like .sheet) is triggered sounds like SwiftUI bridges its menu lifecycle with the host environment. A button tap inside a menu should dismiss the menu regardless of the action's contents, however under that there is a UIKit view waiting? But I can be wrong, I have before and hope other developers jump into this thread.

If you want to avoid dropping the ToolbarItemGroup entirely, you might want to test if using a standard Menu inside a single ToolbarItem (instead of a ToolbarItemGroup) exhibits the same behavior. Sometimes the bridging behavior differs between the two or a slightly workaround is to use the SwiftUI .sheet modifier to present a UIViewControllerRepresentable that acts as a transparent bridge to your UIKit presentation logic, though simply avoiding ToolbarItemGroup?

Thank you for taking the time and going over all that. I’m running out of ideas and hope other developers can look at this thread and find a better solution than going all SwiftUI that will be something I would personally do if I was in your case.

Albert
  Worldwide Developer Relations.

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.

@DTS Engineer Thank you for your response Albert,

Is it timing the issue?

I don't think it is a timing issue because even if I don't trigger a navigation, basically just removing the action code for the "Edit" button, nothing happens, the system doesn't dismiss the context menu when an option is selected automatically. If that would happen I could try to get away with a short delay before presenting. (not ideal though)

you should avoid calling UIKit's present() manually from a SwiftUI button action

Thanks for the suggestion, unfortunately my whole project still relies on UINavigationControllers and UIHostingControllers to manage navigation and presentation, but I want to revisit migrating to full SwiftUI approach since its support has improved greatly with the introduction of NavigationStack.

I tried the following just to see what happens:

Since the context menu doesn't disappear when an option is tapped and no action is triggered, I tried updating a string property annotated with @State and if it has a valid value I simply insert a Text into the view stack, this also didn't make the menu disappear.

Trying a .sheet modifier as you suggested works fine, the action menu disappears and then the sheet is presented.

However, I feel like my use case is also a valid use case though, many iOS projects are still in the process of migrating to SwiftUI, and I know from colleagues and friends that many are still relying on UINavigationController/UIHostingControllers for pushing and presenting views.

Do you think this is something that can be investigated or fixed on your end? If so, I can also send a report through the feedback assistant. If not, then I would just not use toolbar item group until I can migrate to SwiftUI presentation.

Thanks!

Accepted Answer

Thanks for your post and answering my questions as well as providing more information that I didn’t had before. More and more looks like a UIKit timing issue, I know you do not think so, but give me a chance. When I look at the error is the common error when a View has not finished and I try to load a new one. Wrapping SwiftUI views in UIHostingController and managing the navigation stack via UINavigationController is the standard migration path for established UIKit apps. SwiftUI is designed to interoperate smoothly with this architecture, so you shouldn't be forced to migrate your entire presentation layer to SwiftUI just to get a context menu to dismiss properly.

The behavior you are describing—where a menu presented from a ToolbarItemGroup refuses to dismiss upon selection unless a SwiftUI-native presentation modifier (like .sheet) is triggered sounds like SwiftUI bridges its menu lifecycle with the host environment. A button tap inside a menu should dismiss the menu regardless of the action's contents, however under that there is a UIKit view waiting? But I can be wrong, I have before and hope other developers jump into this thread.

If you want to avoid dropping the ToolbarItemGroup entirely, you might want to test if using a standard Menu inside a single ToolbarItem (instead of a ToolbarItemGroup) exhibits the same behavior. Sometimes the bridging behavior differs between the two or a slightly workaround is to use the SwiftUI .sheet modifier to present a UIViewControllerRepresentable that acts as a transparent bridge to your UIKit presentation logic, though simply avoiding ToolbarItemGroup?

Thank you for taking the time and going over all that. I’m running out of ideas and hope other developers can look at this thread and find a better solution than going all SwiftUI that will be something I would personally do if I was in your case.

Albert
  Worldwide Developer Relations.

Thank you for the discussion, and all the details and your recommendations Albert. I just had the chance to try using Menu instead of ToolbarItemGroup and it worked just fine, without any workarounds.

Leaving the final code as a reference:

.toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Menu {
                    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)
                }
            }
}

IFIRC I’ve had success working around this kind of issue by getting the transitionCoordinator and adding an empty animation alongside it and then presenting the view controller in the completion handler.

Fallback and just present it if transition coordinator is nil.

https://developer.apple.com/documentation/uikit/uiviewcontroller/transitioncoordinator?language=objc

I don‘t SwiftUI though so Albert‘s answer might be the better one for your project

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