You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何实现带可交互自定义预览视图的ContextMenu?

SwiftUI ContextMenu预览视图内Reactions组件无法交互的解决办法

在实现iMessage风格聊天功能时,SwiftUI系统自带的ContextMenu预览视图存在限制:预览内的reactionsView点击后会直接关闭整个菜单,无法触发预期的交互动作。

问题代码回顾

MessageView(
    isMyMessage: element.sender.id == currentUserID,
    senderAvatar: Image(.testUser),
    messageType: element.kind,
    previousMessageFromSameSender: messages[previousIndex].sender.id == element.sender.id
).contextMenu {
    Button(),
    Button(),
    Button()
} preview: {
    VStack {
        reactionsView // 无法交互,点击即关闭菜单
        messageView
    }
}

原因分析

SwiftUI的ContextMenu预览视图默认是不可交互的,设计初衷仅用于展示内容,点击预览会直接触发菜单关闭并执行默认提交动作,无法响应内部视图的交互事件。

解决方案

方案1:桥接UIKit实现可交互预览

通过UIKit的UIContextMenuConfiguration自定义上下文菜单,让预览视图支持交互,同时控制点击行为不直接关闭菜单。

首先创建一个SwiftView扩展,封装UIKit的上下文菜单交互:

extension View {
    func interactiveContextMenu(
        preview: @escaping () -> some View,
        menuItems: @escaping () -> UIMenu
    ) -> some View {
        overlay {
            ContextMenuWrapper(preview: preview, menuItems: menuItems)
        }
    }
}

struct ContextMenuWrapper<Preview: View>: UIViewRepresentable {
    let preview: () -> Preview
    let menuItems: () -> UIMenu
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.isUserInteractionEnabled = true
        view.addInteraction(UIContextMenuInteraction(delegate: context.coordinator))
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(preview: preview, menuItems: menuItems)
    }
    
    class Coordinator: NSObject, UIContextMenuInteractionDelegate {
        let preview: () -> Preview
        let menuItems: () -> UIMenu
        
        init(preview: @escaping () -> Preview, menuItems: @escaping () -> UIMenu) {
            self.preview = preview
            self.menuItems = menuItems
        }
        
        func contextMenuInteraction(
            _ interaction: UIContextMenuInteraction,
            configurationForMenuAtLocation location: CGPoint
        ) -> UIContextMenuConfiguration? {
            UIContextMenuConfiguration(identifier: nil, previewProvider: {
                let hostingVC = UIHostingController(rootView: self.preview())
                hostingVC.view.isUserInteractionEnabled = true // 开启预览视图交互
                return hostingVC
            }) { _ in
                self.menuItems()
            }
        }
        
        // 阻止点击预览直接关闭菜单
        func contextMenuInteraction(
            _ interaction: UIContextMenuInteraction,
            willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
            animator: UIContextMenuInteractionCommitAnimating
        ) {
            animator.preferredCommitStyle = .dismiss
            // 如需处理预览点击后的额外动作,可在此添加逻辑
        }
    }
}

然后替换原有的contextMenu使用自定义方法:

MessageView(
    isMyMessage: element.sender.id == currentUserID,
    senderAvatar: Image(.testUser),
    messageType: element.kind,
    previousMessageFromSameSender: messages[previousIndex].sender.id == element.sender.id
).interactiveContextMenu(
    preview: {
        VStack {
            reactionsView // 现在可正常触发交互
            messageView
        }
    },
    menuItems: {
        UIMenu(title: "", children: [
            UIAction(title: "复制") { _ in
                // 处理复制逻辑
            },
            UIAction(title: "删除") { _ in
                // 处理删除逻辑
            }
        ])
    }
)

方案2:用Popover+长按手势替代系统ContextMenu

如果不想依赖UIKit桥接,可直接使用SwiftUI的popover结合长按手势实现完全可控的交互菜单:

@State private var showReactionsMenu = false
@State private var menuAnchorPoint: CGPoint = .zero

// 视图代码
MessageView(
    isMyMessage: element.sender.id == currentUserID,
    senderAvatar: Image(.testUser),
    messageType: element.kind,
    previousMessageFromSameSender: messages[previousIndex].sender.id == element.sender.id
)
.onLongPressGesture(minimumDuration: 0.5) { location in
    menuAnchorPoint = location
    showReactionsMenu = true
}
.popover(
    isPresented: $showReactionsMenu,
    attachmentAnchor: .point(.init(x: menuAnchorPoint.x, y: menuAnchorPoint.y))
) {
    VStack {
        reactionsView // 完全支持交互
        messageView
    }
    .padding(8)
    .background(Color(UIColor.systemBackground))
    .cornerRadius(12)
}

方案对比

  • 方案1:样式更接近系统原生ContextMenu,体验一致,但需要编写UIKit桥接代码。
  • 方案2:纯SwiftUI实现,代码更简洁,但需要自行调整popover样式以匹配系统风格。

内容的提问来源于stack exchange,提问作者Raul Batista

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 12:43:23