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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 10:15:23