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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 02:15:03