如何使UIContextMenu的previewProvider支持点击、滚动等用户交互?
我给某个视图添加了UIContextMenu,想在previewProvider对应的ViewController里添加点击、滚动这类交互,但现在点击预览区域会直接关闭UIContextMenu,怎么实现允许该区域内的用户交互?
原始代码
func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint ) -> UIContextMenuConfiguration? { let previewProvider: UIContextMenuContentPreviewProvider = { () -> UIViewController? in let previewViewController = TestVC(interaction: interaction) return previewViewController } let configuration = UIContextMenuConfiguration( previewProvider: previewProvider ) { [weak self] _ in guard let self = self else { return nil } return self.menu } return configuration } func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying ) -> UITargetedPreview? { return makeTargetedPreview( for: configuration, interaction: interaction ) } func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying ) -> UITargetedPreview? { return makeTargetedPreview( for: configuration, interaction: interaction ) } private func makeTargetedPreview( for configuration: UIContextMenuConfiguration, interaction: UIContextMenuInteraction, presentReactions: Bool ) -> UITargetedPreview? { guard let view = interaction.view else { return nil } let parameters = UIPreviewParameters() parameters.backgroundColor = .clear return UITargetedPreview(view: view, parameters: parameters) } class TestVC: UIViewController { private let space: CGFloat = 5 private var contentView: UIView! init(interaction: UIContextMenuInteraction) { let xOffset: CGFloat if let interactionView = interaction.view, let view = interactionView.snapshotView(afterScreenUpdates: true) { self.contentView = view xOffset = interactionView.frame.origin.x } else { self.contentView = UIView() xOffset = 0 } super.init(nibName: nil, bundle: nil) view.addSubview(contentView) contentView.frame.origin.x = xOffset contentView.frame.origin.y = space } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear preferredContentSize = .init( width: view.bounds.width, height: contentView.bounds.height + space ) // try to custom add gestures but didnt work setupGestureRecognizers() } private func setupGestureRecognizers() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) view.addGestureRecognizer(tapGestureRecognizer) } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { print("handleTap") } }
解决方案
要让预览视图支持自定义交互,需要两步关键操作:
阻止预览视图的默认点击关闭行为
实现UIContextMenuInteractionDelegate的contextMenuInteraction(_:willPerformPreviewActionForMenuWith:animator:)方法,取消默认的动画器操作,这样点击预览区域就不会自动关闭菜单:func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { // 取消默认提交动画,阻止点击预览关闭菜单 animator.cancel() // 可在这里添加自定义逻辑,比如特定条件下手动关闭菜单 }确保预览控制器的视图能接收触摸
显式开启视图的用户交互权限,同时可调整手势优先级或直接添加需要的交互组件(如滚动视图):- 确保
contentView.isUserInteractionEnabled = true - 添加
UIScrollView可直接支持滚动交互
- 确保
修改后的完整代码
// 新增代理方法,阻止默认关闭行为 func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { animator.cancel() } // 原有代理方法保持不变 func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint ) -> UIContextMenuConfiguration? { let previewProvider: UIContextMenuContentPreviewProvider = { () -> UIViewController? in let previewViewController = TestVC(interaction: interaction) return previewViewController } let configuration = UIContextMenuConfiguration( previewProvider: previewProvider ) { [weak self] _ in guard let self = self else { return nil } return self.menu } return configuration } func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying ) -> UITargetedPreview? { return makeTargetedPreview( for: configuration, interaction: interaction ) } func contextMenuInteraction( _ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying ) -> UITargetedPreview? { return makeTargetedPreview( for: configuration, interaction: interaction ) } // 修复参数不匹配问题,添加默认参数 private func makeTargetedPreview( for configuration: UIContextMenuConfiguration, interaction: UIContextMenuInteraction, presentReactions: Bool = false ) -> UITargetedPreview? { guard let view = interaction.view else { return nil } let parameters = UIPreviewParameters() parameters.backgroundColor = .clear return UITargetedPreview(view: view, parameters: parameters) } class TestVC: UIViewController { private let space: CGFloat = 5 private var contentView: UIView! init(interaction: UIContextMenuInteraction) { let xOffset: CGFloat if let interactionView = interaction.view, let view = interactionView.snapshotView(afterScreenUpdates: true) { self.contentView = view self.contentView.isUserInteractionEnabled = true // 显式开启交互 xOffset = interactionView.frame.origin.x } else { self.contentView = UIView() xOffset = 0 } super.init(nibName: nil, bundle: nil) view.addSubview(contentView) contentView.frame.origin.x = xOffset contentView.frame.origin.y = space } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear view.isUserInteractionEnabled = true // 确保主视图允许交互 // 修复初始化时view.bounds.width为0的问题 preferredContentSize = .init( width: UIScreen.main.bounds.width, height: contentView.bounds.height + space ) setupGestureRecognizers() } private func setupGestureRecognizers() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) tapGestureRecognizer.cancelsTouchesInView = false // 避免拦截子视图触摸 view.addGestureRecognizer(tapGestureRecognizer) // 示例:添加滚动视图支持滚动交互 let scrollView = UIScrollView(frame: view.bounds) scrollView.contentSize = CGSize(width: view.bounds.width, height: contentView.bounds.height * 2) scrollView.addSubview(contentView) contentView.frame = scrollView.bounds view.addSubview(scrollView) } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { print("handleTap") // 如需手动关闭菜单,可调用以下逻辑 // if let interaction = findContextMenuInteraction() { // interaction.dismissMenu() // } } // 辅助方法:查找对应的UIContextMenuInteraction(用于手动关闭菜单) private func findContextMenuInteraction() -> UIContextMenuInteraction? { var responder: UIResponder? = self while responder != nil { if let view = responder as? UIView { return view.interactions.compactMap { $0 as? UIContextMenuInteraction }.first } responder = responder?.next } return nil } }
说明
- 新增的
willPerformPreviewActionForMenuWith是核心,它阻断了系统默认的点击关闭逻辑。 - 修复了原代码中
makeTargetedPreview方法调用的参数不匹配问题。 - 调整了预览视图的尺寸计算,避免初始化时宽度为0的异常。
- 示例中添加了滚动视图,你可根据需求替换为自己的交互组件。
内容的提问来源于stack exchange,提问作者akex jeer
相关产品推荐
相关产品推荐

