SwiftUI:如何通过手势拖拽数组中的对象?
问题分析与解决方案
问题核心在于值类型的数组更新逻辑:ScreenElement是结构体(值类型),数组也是值类型,直接修改数组内的结构体元素时,若未正确触发模型的更新通知,SwiftUI无法感知到变化,导致视图不刷新,拖拽手势仅能响应一次。以下是两种可行的实现方案:
方案1:保持结构体类型,正确更新数组
核心思路
给ScreenElement添加稳定的唯一ID,修改元素时通过替换数组内的结构体实例触发模型更新,同时确保ForEach使用稳定ID来避免视图频繁重建。
代码实现
// 定义带稳定ID的结构体 struct ScreenElement { let id: UUID var offset: CGSize } // 可观察模型 @Observable class DraggerModel { var elements: [ScreenElement] = [] init() { // 初始化测试元素 elements = [ ScreenElement(id: UUID(), offset: .zero), ScreenElement(id: UUID(), offset: CGSize(width: 100, height: 100)) ] } // 更新元素偏移量:通过创建新实例替换数组内的旧元素 func updateOffset(for elementId: UUID, newOffset: CGSize) { guard let index = elements.firstIndex(where: { $0.id == elementId }) else { return } elements[index] = ScreenElement(id: elementId, offset: newOffset) } } // 视图实现 struct DragDemoView: View { @State private var model = DraggerModel() var body: some View { ZStack { ForEach(model.elements, id: \.id) { element in Circle() .frame(width: 50, height: 50) .position(x: element.offset.width, y: element.offset.height) .gesture( DragGesture() .onChanged { gesture in // 计算当前偏移量:原偏移量 + 拖拽增量 let currentOffset = CGSize( width: element.offset.width + gesture.translation.width, height: element.offset.height + gesture.translation.height ) model.updateOffset(for: element.id, newOffset: currentOffset) } ) } } } }
方案2:将ScreenElement改为引用类型(类)
核心思路
把ScreenElement改为类(引用类型),内部用@Published修饰偏移量属性,修改时直接更新类实例的属性,无需替换数组元素,SwiftUI能直接感知到属性变化并刷新视图。
代码实现
// 定义可观察的类类型 class ScreenElement: ObservableObject { let id: UUID @Published var offset: CGSize init(id: UUID, offset: CGSize) { self.id = id self.offset = offset } } // 可观察模型 @Observable class DraggerModel { var elements: [ScreenElement] = [] init() { elements = [ ScreenElement(id: UUID(), offset: .zero), ScreenElement(id: UUID(), offset: CGSize(width: 100, height: 100)) ] } // 直接更新类实例的偏移量 func updateOffset(for elementId: UUID, newOffset: CGSize) { guard let element = elements.first(where: { $0.id == elementId }) else { return } element.offset = newOffset } } // 视图实现(与方案1基本一致) struct DragDemoView: View { @State private var model = DraggerModel() var body: some View { ZStack { ForEach(model.elements, id: \.id) { element in Circle() .frame(width: 50, height: 50) .position(x: element.offset.width, y: element.offset.height) .gesture( DragGesture() .onChanged { gesture in let currentOffset = CGSize( width: element.offset.width + gesture.translation.width, height: element.offset.height + gesture.translation.height ) model.updateOffset(for: element.id, newOffset: currentOffset) } ) .environmentObject(element) // 可选:若视图需监听element的变化 } } } }
关键注意事项
- 无论哪种方案,拖拽时都要基于元素当前的偏移量加上拖拽增量,不能直接使用
gesture.translation,否则元素会频繁跳回原点。 - 禁止用
\.self作为ForEach的ID:结构体的偏移量变化会导致\.self的哈希值改变,触发视图销毁重建,直接导致手势失效。
内容的提问来源于stack exchange,提问作者coco
相关产品推荐
相关产品推荐

