SwiftUI中如何检测ContextMenu的每次显示?解决onAppear仅触发一次的问题
Got it, I ran into this exact issue a while back! The problem with using onAppear inside the contextMenu content is that SwiftUI caches that menu view hierarchy after the first time it's shown—so onAppear only fires once, even if you open the menu multiple times.
Luckily, there are a couple of solid ways to detect every time the ContextMenu is about to appear. Here are the most reliable approaches:
1. Use UIKit's UIContextMenuInteraction (iOS 13+)
Since SwiftUI's ContextMenu is built on top of UIKit's UIContextMenuInteraction, we can create a custom ViewModifier that hooks into the interaction's delegate methods to detect when the menu is about to display. This works for all iOS versions that support ContextMenus (iOS 13+).
First, create the delegate and modifier:
import SwiftUI class ContextMenuDelegate: NSObject, UIContextMenuInteractionDelegate { var willDisplayMenu: (() -> Void)? func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { // This gets called EVERY time the menu is about to show willDisplayMenu?() } } struct ContextMenuPreActionModifier: ViewModifier { let preAction: () -> Void func body(content: Content) -> some View { content .background( UIViewRepresentable { view in let interaction = UIContextMenuInteraction(delegate: ContextMenuDelegate()) if let delegate = interaction.delegate as? ContextMenuDelegate { delegate.willDisplayMenu = preAction } view.addInteraction(interaction) } ) } } extension View { // Convenience extension to make usage cleaner func onContextMenuWillAppear(perform action: @escaping () -> Void) -> some View { self.modifier(ContextMenuPreActionModifier(preAction: action)) } }
Then use it in your view like this:
var body: some View { Text("Hello, world!") .contextMenu { VStack { Button(action: { }) { Text("Normal Colors") } Button(action: { }) { Text("Inverted Colors") } } } .onContextMenuWillAppear { // This code runs EVERY time the context menu is about to appear print("Context menu is opening—time to run my pre-action!") } }
2. iOS 16+ Only: Use Menu with onPresented
If you're targeting iOS 16 or later, SwiftUI added much cleaner support for menu presentation callbacks. You can replace the contextMenu with a Menu set to trigger on right-click, then use the onPresented closure to run your pre-action:
var body: some View { Text("Hello, world!") .menu { Button(action: { }) { Text("Normal Colors") } Button(action: { }) { Text("Inverted Colors") } } .menuTrigger(.contextMenu) // Makes it open on right-click/long press .onPresented { // Runs every time the context menu is opened print("Context menu is appearing—pre-action time!") } }
This approach is way simpler if you don't need to support older iOS versions. The onPresented closure is designed to fire every time the menu is displayed, so you don't have to deal with any view caching issues.
Key Takeaway
- Use the UIKit-based modifier if you need iOS 13+ compatibility.
- Use the iOS 16+
Menuapproach for a cleaner, pure-SwiftUI solution.
内容的提问来源于stack exchange,提问作者jdleung

