SwiftUI如何在两个可移动视图之间绘制动态连线?
实现方案
你原有方案的核心问题在于SwiftUI的View是描述性的值类型结构体,并非UIKit中持有运行时属性的视图实例,直接传递View实例无法获取实时位置信息。正确实现逻辑如下:
步骤1:改造ViewModel,管理位置与选中状态
给每个可移动视图分配唯一ID,在ViewModel中维护所有视图的实时位置映射,以及用户选中的两个视图ID:
import SwiftUI import Combine let heartOnTap = Notification.Name("heartOnTap") class ViewModel: ObservableObject { // 存储所有Heart的实时全局位置 @Published var heartPositions: [UUID: CGPoint] = [:] // 存储用户选中的两个Heart ID @Published var selectedIds: [UUID] = [] init() { NotificationCenter.default.addObserver(self, selector: #selector(self.didTap(_:)), name: heartOnTap, object: nil) } @objc func didTap(_ notification: NSNotification) { guard let id = notification.userInfo?["id"] as? UUID else { return } // 已选两个则重置重选 if selectedIds.count == 2 { selectedIds.removeAll() } // 禁止重复选中同一个视图 guard !selectedIds.contains(id) else { return } selectedIds.append(id) } // 对外提供连线的两个端点坐标 var linePoints: (start: CGPoint?, end: CGPoint?) { guard selectedIds.count == 2, let start = heartPositions[selectedIds[0]], let end = heartPositions[selectedIds[1]] else { return (nil, nil) } return (start, end) } }
步骤2:定义位置同步用的PreferenceKey
通过PreferenceKey获取每个Heart视图在全局坐标系的实时位置:
struct HeartPositionKey: PreferenceKey { static var defaultValue: [UUID: CGPoint] = [:] static func reduce(value: inout [UUID : CGPoint], nextValue: () -> [UUID : CGPoint]) { value.merge(nextValue()) { $1 } } }
步骤3:改造Heart与可移动组件
点击时仅传递视图唯一ID,拖拽移动时自动上报最新位置:
struct Heart: View { let id: UUID // 绑定可移动卡片的拖拽偏移量 @Binding var offset: CGSize var body: some View { MovableCard("heart.fill", offset: $offset) .background( // 读取当前视图的全局中心位置并上报 GeometryReader { proxy in Color.clear .preference(key: HeartPositionKey.self, value: [ id: CGPoint( x: proxy.frame(in: .global).midX, y: proxy.frame(in: .global).midY ) ]) } ) .onTapGesture { NotificationCenter.default.post(name: heartOnTap, object: nil, userInfo: ["id": id]) } } } // 可拖拽移动的卡片组件实现示例 struct MovableCard: View { let icon: String @Binding var offset: CGSize @State private var lastOffset: CGSize = .zero init(_ icon: String, offset: Binding<CGSize>) { self.icon = icon self._offset = offset } var body: some View { Image(systemName: icon) .font(.largeTitle) .foregroundColor(.red) .frame(width: 60, height: 60) .background(Circle().fill(.white)) .shadow(radius: 3) .offset(offset) .gesture( DragGesture() .onChanged { value in offset = CGSize( width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height ) } .onEnded { _ in lastOffset = offset } ) } }
步骤4:改造BoardView,绘制连线
监听所有Heart的位置变化,同步到ViewModel后绘制连线:
struct BoardView: View { @StateObject var viewModel = ViewModel() // 两个Heart的拖拽偏移量,也可以放到ViewModel中统一管理 @State private var heart1Offset = CGSize.zero @State private var heart2Offset = CGSize.zero private let heart1Id = UUID() private let heart2Id = UUID() var body: some View { ZStack{ // 绘制两个选中视图的连线 Path{ path in guard let start = viewModel.linePoints.start, let end = viewModel.linePoints.end else { return } path.move(to: start) path.addLine(to: end) } .stroke(Color.blue, lineWidth: 2) Heart(id: heart1Id, offset: $heart1Offset) Heart(id: heart2Id, offset: $heart2Offset) } // 监听所有Heart的位置变化,同步到ViewModel .onPreferenceChange(HeartPositionKey.self) { positions in viewModel.heartPositions = positions } } }
方案特性
- 视图拖拽移动时会自动同步最新位置,连线会跟随视图实时更新
- 无需传递View实例,仅通过唯一ID匹配视图位置,符合SwiftUI的数据流规范
- 支持扩展更多可移动视图,无需修改核心逻辑
内容的提问来源于stack exchange,提问作者Chat Dp
相关产品推荐
相关产品推荐

