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

如何使UIContextMenu的previewProvider支持点击、滚动等用户交互?

如何让UIContextMenu的预览视图支持用户交互

我给某个视图添加了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")
    }
}

解决方案

要让预览视图支持自定义交互,需要两步关键操作:

  1. 阻止预览视图的默认点击关闭行为
    实现UIContextMenuInteractionDelegate的contextMenuInteraction(_:willPerformPreviewActionForMenuWith:animator:)方法,取消默认的动画器操作,这样点击预览区域就不会自动关闭菜单:

    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        // 取消默认提交动画,阻止点击预览关闭菜单
        animator.cancel()
        // 可在这里添加自定义逻辑,比如特定条件下手动关闭菜单
    }
    
  2. 确保预览控制器的视图能接收触摸
    显式开启视图的用户交互权限,同时可调整手势优先级或直接添加需要的交互组件(如滚动视图):

    • 确保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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 15:07:54