iOS15+下SwiftUI中实现动态UIContextMenu菜单与预览
我需要实现一个支持自定义预览的.contextMenu修饰符,要求兼容iOS15.0及以上版本(iOS16.0+原生已支持该功能)。参考相关博客完成基础功能调整后,发现容器SwiftUI视图的@State变量变更无法触发菜单及预览内容更新。尝试用iOS15.0引入的UIDeferredMenuElement.uncached解决时,出现错误提示:
[UILog] Called -[UIContextMenuInteraction updateVisibleMenuWithBlock:] while no context menu is visible. This won't do anything
且菜单完全无法正常工作。现在想知道如何实现每次激活菜单时重建菜单与预览内容。
问题复现代码
struct TestOptions: OptionSet { let rawValue: UInt static let Test = TestOptions(rawValue: 1 << 0) static let Test0 = TestOptions(rawValue: 1 << 1) static let Test1 = TestOptions(rawValue: 1 << 2) static let all: TestOptions = [.Test, .Test0, .Test1] } // ContentView.swift struct ContentView: View { @State private var option: TestOptions = .Test var body: some View { NavigationView { Text("选中的测试选项:\(option.rawValue)") .previewContextMenu( preview: Text("\(option.rawValue)") ) { PreviewContextAction(title: "Test", isSelected: self.option == .Test) { self.option = .Test } PreviewContextAction(title: "Test 0", isSelected: self.option == .Test0) { self.option = .Test0 } PreviewContextAction(title: "Test 1", isSelected: self.option == .Test1) { self.option = .Test1 } } } } }
注:已修改PreviewContextAction代码以支持State选中标记。
解决方案
核心思路
问题根源在于自定义ContextMenu实现时,没有在菜单激活时重新捕获最新的State状态,而是复用了旧的快照。要解决这个问题,需要确保每次触发ContextMenu时,都基于当前最新的视图状态重新构建菜单内容和预览视图,避免提前缓存或错误调用更新方法。
具体实现步骤
修改自定义
previewContextMenu修饰符的UIKit桥接逻辑
在负责创建UIContextMenuInteraction的SwiftUI视图扩展中,将菜单和预览的构建逻辑延迟到菜单即将显示时执行:struct PreviewContextMenuModifier<Preview: View, Content: View>: ViewModifier { let preview: () -> Preview let menuContent: () -> Content func body(content: Content) -> some View { content .background( ContextMenuUIKitBridge( preview: preview, menuContent: menuContent ) ) } } private struct ContextMenuUIKitBridge<Preview: View, Content: View>: UIViewRepresentable { let preview: () -> Preview let menuContent: () -> Content func makeUIView(context: Context) -> UIView { let view = UIView() view.isUserInteractionEnabled = true let interaction = UIContextMenuInteraction(delegate: context.coordinator) view.addInteraction(interaction) return view } func updateUIView(_ uiView: UIView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(preview: preview, menuContent: menuContent) } class Coordinator: NSObject, UIContextMenuInteractionDelegate { private let preview: () -> Preview private let menuContent: () -> Content init(preview: @escaping () -> Preview, menuContent: @escaping () -> Content) { self.preview = preview self.menuContent = menuContent } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { return UIContextMenuConfiguration(identifier: nil, previewProvider: { // 每次预览显示时,基于当前State创建最新的预览视图控制器 let previewController = UIHostingController(rootView: self.preview()) previewController.preferredContentSize = CGSize(width: 300, height: 100) return previewController }) { _ in // 每次菜单激活时,重新构建UIMenu let menuBuilder = UIHostingController(rootView: self.menuContent()) let menuItems = menuBuilder.rootView.children.compactMap { child -> UIAction? in guard let action = child as? PreviewContextAction else { return nil } return UIAction(title: action.title, state: action.isSelected ? .on : .off) { _ in action.action() } } return UIMenu(children: menuItems) } } } } // 扩展View,添加previewContextMenu方法 extension View { func previewContextMenu<Preview: View, Content: View>(preview: @escaping () -> Preview, @ViewBuilder menuContent: @escaping () -> Content) -> some View { self.modifier(PreviewContextMenuModifier(preview: preview, menuContent: menuContent)) } } // PreviewContextAction定义 struct PreviewContextAction: View { let title: String let isSelected: Bool let action: () -> Void var body: some View { EmptyView() // 仅作为数据载体,实际菜单由UIKit桥接层构建 } }关键调整点
- 将
preview和menuContent改为逃逸闭包,确保每次调用时都能获取最新的State值 - 在
contextMenuInteraction(_:configurationForMenuAtLocation:)方法中,每次菜单激活时重新构建预览控制器和UIMenu,而非提前缓存 - 移除任何调用
updateVisibleMenuWithBlock的逻辑,该方法仅在菜单已显示时有效,我们的目标是每次激活时重建菜单,无需更新已显示的菜单
- 将
验证效果
修改后,每次长按触发ContextMenu时,都会基于当前最新的@State值构建预览视图和菜单选项的选中状态,解决了State变更不更新的问题,同时避免了错误日志的出现。
内容的提问来源于stack exchange,提问作者Baffo rasta

