iOS 13 Context Menu:点击预览时如何实现菜单平滑关闭?
实现上下文菜单预览点击后的反向关闭动画
我完全懂你说的那个生硬的全屏展开动画——确实很破坏体验。要实现和唤起菜单反向的关闭动画,核心是拦截默认的预览点击行为,自定义动画逻辑让预览视图缩回到原单元格的位置,而非全屏展开。下面是具体的实现步骤:
1. 拦截默认的预览提交动作
确保你的UIViewController遵循UIContextMenuInteractionDelegate(如果用UITableView内置上下文菜单,代理已自动关联,我们只需重写特定方法)。
实现willPerformPreviewActionForMenuWithConfiguration方法,这个方法会在用户点击预览视图时触发,我们可以在这里取消默认行为,替换成自定义动画:
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { // 取消系统默认的动画和跳转行为 animator.cancel() // 获取预览视图控制器(如果用快照,可调整为操作快照视图) guard let previewVC = animator.previewViewController else { // 兜底:直接关闭菜单 interaction.dismissMenu() return } // 从配置中拿到对应单元格索引,获取原单元格在窗口中的位置 guard let indexPath = configuration.identifier as? IndexPath, let cell = tableView.cellForRow(at: indexPath), let targetFrame = tableView.convert(cell.frame, to: nil) else { interaction.dismissMenu() return } // 执行反向动画:把预览视图缩回到原单元格位置并淡出 UIView.animate(withDuration: 0.3, curve: .easeInOut) { previewVC.view.frame = targetFrame previewVC.view.alpha = 0 } completion: { _ in // 动画结束后关闭菜单并移除预览VC interaction.dismissMenu() previewVC.dismiss(animated: false) } }
2. 匹配预览视图与单元格的初始大小
为了让反向动画更自然,需要确保预览视图的初始大小和原单元格一致。在创建上下文菜单配置时,设置预览控制器的preferredContentSize:
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { return UIContextMenuConfiguration(identifier: indexPath, previewProvider: { let previewVC = YourCustomPreviewViewController() // 让预览VC的大小和对应单元格完全一致 previewVC.preferredContentSize = tableView.cellForRow(at: indexPath)?.frame.size ?? CGSize(width: tableView.bounds.width, height: 60) return previewVC }) { _ in // 返回你的菜单动作列表 return UIMenu(title: "", children: [ UIAction(title: "编辑", handler: { _ in // 处理编辑逻辑 }), UIAction(title: "删除", attributes: .destructive, handler: { _ in // 处理删除逻辑 }) ]) } }
3. 针对快照预览的调整(可选)
如果没有用单独的预览VC,而是直接返回单元格的快照视图,可以调整动画逻辑直接操作快照视图:
// 在previewProvider里返回快照 previewProvider: { guard let cell = tableView.cellForRow(at: indexPath) else { return nil } let snapshot = cell.snapshotView(afterScreenUpdates: true)! snapshot.frame = cell.frame return snapshot }
然后在代理方法里把previewViewController换成animator.previewView来操作:
guard let previewView = animator.previewView else { interaction.dismissMenu() return } // 后续动画逻辑直接操作previewView即可
关键注意事项
- 该方案仅适用于iOS 13及以上(上下文菜单最低支持版本),若需兼容更低版本,记得做版本判断。
- 动画时长可根据需求调整,0.3秒是贴近系统默认动画的时长。
- 若单元格为动态高度,确保获取的
targetFrame是当前单元格在窗口中的真实位置,避免动画错位。
内容的提问来源于stack exchange,提问作者mmklug
相关产品推荐
相关产品推荐

