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

SwiftUI Canvas多视图拖拽定位问题:子视图手势不识别

SwiftUI Canvas 多视图拖拽重定位解决方案

核心问题分析

Canvas 中的 Symbol 并非真实的交互式 View,而是离线渲染的绘制内容,因此直接给 Symbol 内的 ItemView 添加手势不会生效。要实现单个视图拖拽,需要在 Canvas 外层处理全局手势,并通过命中测试识别选中的 Item,再更新其位置。

解决方案步骤

  1. 标记选中目标:用 NSManagedObjectID 记录当前正在拖拽的 Item
  2. 命中测试判断:拖拽开始时,计算每个 Item 的绘制边界,确认点击位置所属的 Item
  3. 精准更新位置:基于拖拽偏移量,修改选中 Item 的 Core Data 位置属性(而非全局统一偏移)

修改后的完整代码

struct TestCanvasView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(fetchRequest: Item.all) var items
    
    var currentItems: [Item] {
        items.filter { $0.parent == nil }
    }
    
    @State private var selectedItemID: NSManagedObjectID?
    @State private var initialItemPosition: CGPoint?

    var body: some View {
        Canvas { context, size in
            for currentItem in currentItems {
                guard let itemView = context.resolveSymbol(id: currentItem.objectID) else { continue }
                // 以 Core Data 中存储的位置为中心绘制 Item
                context.draw(itemView, at: currentItem.itemPosition)
            }
        } symbols: {
            ForEach(currentItems) { item in
                ItemView(item: item)
                    .tag(item.objectID)
            }
        }
        .gesture(
            DragGesture()
                .onStarted { value in
                    // 遍历所有 Item,执行命中测试
                    for item in currentItems {
                        guard let itemView = context.resolveSymbol(id: item.objectID) else { continue }
                        let itemSize = context.measure(itemView)
                        // 计算 Item 的实际绘制边界(默认以中心为锚点,需偏移半宽高)
                        let itemBounds = CGRect(
                            x: item.itemPosition.x - itemSize.width / 2,
                            y: item.itemPosition.y - itemSize.height / 2,
                            width: itemSize.width,
                            height: itemSize.height
                        )
                        
                        if itemBounds.contains(value.location) {
                            selectedItemID = item.objectID
                            initialItemPosition = item.itemPosition
                            break
                        }
                    }
                }
                .onChanged { value in
                    guard let selectedID = selectedItemID,
                          let initialPos = initialItemPosition,
                          let selectedItem = currentItems.first(where: { $0.objectID == selectedID }) else {
                        return
                    }
                    
                    // 计算新位置:初始位置 + 拖拽相对偏移量
                    let newPosition = CGPoint(
                        x: initialPos.x + value.translation.width,
                        y: initialPos.y + value.translation.height
                    )
                    
                    // 更新 Core Data 并保存,确保位置持久化
                    selectedItem.itemPosition = newPosition
                    try? viewContext.save()
                }
                .onEnded { _ in
                    // 拖拽结束,清除选中状态
                    selectedItemID = nil
                    initialItemPosition = nil
                }
        )
    }
}

关键细节说明

  • 命中测试逻辑:通过 context.measure(_:) 获取 Symbol 的实际尺寸,结合 Item 的中心位置计算出可交互边界,判断拖拽起始点是否落在目标范围内。
  • 位置计算方式:使用拖拽的 translation(相对于起始点的偏移量)计算新位置,避免直接使用屏幕绝对位置导致的视图跳动问题。
  • 数据同步处理:直接修改 Core Data 中 Item 的位置属性并保存上下文,保证拖拽后的位置能持久化存储。

内容的提问来源于stack exchange,提问作者devon't

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 12:35:24