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