macOS SwiftUI实现单击选单词、拖拽框选多单词方案
解决方案:获取拖拽选框内的单词
要实现拖拽选框选中区域内的单词,核心是获取每个单词的实际显示区域,并在拖拽结束时判断这些区域是否与选框重叠。以下是具体实现步骤:
1. 存储每个单词的显示Frame
为每个Text组件添加GeometryReader背景,捕获其在父视图中的位置和尺寸,并存入数组统一管理。
2. 计算选框的实际区域
将当前代码中用boxX/boxY(中心点坐标)和boxWidth/boxHeight定义的选框,转换为标准CGRect(包含左上角坐标、宽高),方便后续区域相交判断。
3. 拖拽结束时匹配选框内的单词
在DragGesture的结束回调中,遍历所有单词的Frame,判断是否与选框区域相交,批量更新选中状态。
修改后的完整代码
struct WordDetectionTextView: View { let words : [String] @State private var selections: [Int : Bool] = [:] @State private var wordFrames: [Int: CGRect] = [:] // 存储每个单词的显示区域 @State private var dragStartPoint: CGPoint? @State private var dragCurrentPoint: CGPoint? @State private var boxX: CGFloat? @State private var boxY: CGFloat? @State private var boxWidth: CGFloat? @State private var boxHeight: CGFloat? var body : some View { var width = CGFloat.zero var height = CGFloat.zero return GeometryReader { parentGeo in ZStack(alignment: .topLeading) { ForEach(0..<words.count, id: \.self) { i in Text(words[i]) .foregroundStyle((selections[i] ?? false) ? Color.red : Color.white) .font(.largeTitle) .padding([.horizontal, .vertical], 4) .onTapGesture { let selectionState = selections[i] ?? false selections[i] = !selectionState } // 捕获单词的显示Frame .background( GeometryReader { geo in Color.clear.onAppear { let frame = geo.frame(in: .named("WordContainer")) wordFrames[i] = frame } } ) .alignmentGuide(.leading, computeValue: { d in if (abs(width - d.width) > parentGeo.size.width) { width = 0 height -= d.height } let result = width if i < self.words.count-1 { width -= d.width } else { width = 0 //last item } return result }) .alignmentGuide(.top, computeValue: {d in let result = height if i >= words.count-1 { height = 0 // last item } return result }) } if let boxX, let boxY, let boxWidth, let boxHeight { Color.white .opacity(0.5) .position(x: boxX, y: boxY) .frame(width: boxWidth, height: boxHeight) } } .coordinateSpace(name: "WordContainer") // 统一坐标空间,确保Frame计算准确 } .frame(height: 300) .gesture( DragGesture(minimumDistance: 0.0) .onChanged { value in dragStartPoint = value.startLocation dragCurrentPoint = value.location updateBox() } .onEnded { value in guard let start = dragStartPoint, let end = dragCurrentPoint else { dragStartPoint = nil dragCurrentPoint = nil updateBox() return } // 计算选框的实际CGRect let boxRect = CGRect( x: min(start.x, end.x), y: min(start.y, end.y), width: abs(start.x - end.x), height: abs(start.y - end.y) ) // 更新选中状态:判断单词Frame是否与选框相交 for (index, frame) in wordFrames { selections[index] = frame.intersects(boxRect) } dragStartPoint = nil dragCurrentPoint = nil updateBox() } ) } private func updateBox() { updateBoxWidth() updateBoxHeight() updateBoxX() updateBoxY() } private func updateBoxX() { guard let startX = dragStartPoint?.x, let currentX = dragCurrentPoint?.x, let boxWidth else { boxX = nil return } if startX < currentX { boxX = startX + (boxWidth / 2.0) } else { boxX = startX - (boxWidth / 2.0) } } private func updateBoxY() { guard let startY = dragStartPoint?.y, let currentY = dragCurrentPoint?.y, let boxHeight else { boxY = nil return } if startY < currentY { boxY = startY + (boxHeight / 2.0) } else { boxY = startY - (boxHeight / 2.0) } } private func updateBoxWidth() { guard let startX = dragStartPoint?.x, let currentX = dragCurrentPoint?.x else { boxWidth = nil return } boxWidth = abs(startX - currentX) } private func updateBoxHeight() { guard let startY = dragStartPoint?.y, let currentY = dragCurrentPoint?.y else { boxHeight = nil return } boxHeight = abs(startY - currentY) } }
关键说明
- 坐标空间统一:通过
.coordinateSpace(name: "WordContainer")确保所有单词的Frame计算基于同一个父视图坐标系,避免层级偏差。 - Frame捕获时机:在
onAppear中获取单词Frame,确保布局完成后记录的位置和尺寸准确。 - 区域相交判断:使用
CGRect.intersects(_:)方法快速判断单词是否处于选框范围内,实现批量选中逻辑。
内容的提问来源于stack exchange,提问作者KevinF
相关产品推荐
相关产品推荐

