SwiftUI iOS端实现Menu式滑动悬停效果及Menu程序化展示问询
一、模拟滑动悬停的背景变色效果
iOS没有原生hover事件,需通过DragGesture结合视图坐标跟踪模拟触摸悬停效果。核心逻辑是:获取按钮全局屏幕坐标,在拖拽手势中判断触摸点是否落在按钮范围内,以此切换背景色状态。
代码实现
struct HoverableMenuButton: View { @State private var isHovering = false @State private var buttonGlobalFrame = CGRect.zero let menuContent: () -> Menu<Text> var body: some View { menuContent() .background(isHovering ? Color.blue.opacity(0.3) : Color.gray.opacity(0.1)) .cornerRadius(8) .overlay( // 获取按钮全局坐标 GeometryReader { proxy in Color.clear .preference(key: ButtonFrameKey.self, value: proxy.frame(in: .global)) } ) .onPreferenceChange(ButtonFrameKey.self) { frame in buttonGlobalFrame = frame } .gesture( DragGesture(minimumDistance: 0) .onChanged { gesture in // 判断触摸点是否在按钮范围内 isHovering = buttonGlobalFrame.contains(gesture.location) } .onEnded { _ in isHovering = false } ) } } // 自定义PreferenceKey传递视图坐标 struct ButtonFrameKey: PreferenceKey { static var defaultValue: CGRect = .zero static func reduce(value: inout CGRect, nextValue: () -> CGRect) { value = nextValue() } } // 使用示例 struct ContentView: View { var body: some View { HoverableMenuButton { Menu("操作") { Button("选项1") { print("选项1触发") } Button("选项2") { print("选项2触发") } } } } }
关键点说明
- 用
GeometryReader和自定义PreferenceKey获取按钮全局屏幕坐标,确保触摸位置判断准确。 DragGesture(minimumDistance: 0)响应所有触摸移动,包括初始触摸按下瞬间。- 触摸结束后重置
isHovering状态,避免残留高亮。
二、程序化展示Menu
SwiftUI原生Menu无直接isPresented参数控制显示,需借助UIKit的UIContextMenuInteraction实现程序化触发,再通过UIViewRepresentable集成到SwiftUI中。
代码实现
struct ProgrammableContextMenu: UIViewRepresentable { let menuActions: [UIAction] let buttonTitle: String @Binding var shouldShowMenu: Bool func makeUIView(context: ContextMenuContext) -> UIButton { let button = UIButton(type: .system) button.setTitle(buttonTitle, for: .normal) button.addInteraction(UIContextMenuInteraction(delegate: context.coordinator)) return button } func updateUIView(_ uiView: UIButton, context: ContextMenuContext) { context.coordinator.menuActions = menuActions // 当shouldShowMenu变为true时触发菜单 if shouldShowMenu { context.coordinator.showMenu(from: uiView) shouldShowMenu = false // 重置状态 } } func makeCoordinator() -> Coordinator { Coordinator() } class Coordinator: NSObject, UIContextMenuInteractionDelegate { var menuActions: [UIAction] = [] func showMenu(from button: UIButton) { guard let interaction = button.interactions.first(where: { $0 is UIContextMenuInteraction }) as? UIContextMenuInteraction else { return } interaction.showMenu(at: button.bounds.center, in: button) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in UIMenu(title: "", children: self.menuActions) } } } } extension CGRect { var center: CGPoint { CGPoint(x: midX, y: midY) } } // 使用示例 struct ContentView: View { @State private var triggerMenu = false var body: some View { VStack { ProgrammableContextMenu( menuActions: [ UIAction(title: "选项1") { _ in print("选项1触发") }, UIAction(title: "选项2") { _ in print("选项2触发") } ], buttonTitle: "操作", shouldShowMenu: $triggerMenu ) Button("手动触发菜单") { triggerMenu = true } } } }
关键点说明
- 通过
@Binding的shouldShowMenu状态控制菜单程序化展示,状态变为true时调用showMenu方法。 - 借助UIKit的
UIContextMenuInteraction实现菜单自定义触发,完全兼容iOS系统菜单样式。 - 可结合第一部分滑动悬停逻辑,在
isHovering变为true时设置shouldShowMenu = true,实现滑动到按钮时自动弹出菜单。
内容的提问来源于stack exchange,提问作者alex king
相关产品推荐
相关产品推荐

