Chat Dp
Chat Dp

Reputation: 627

Is it possible to draw line from view to another view

Goal Example

enter image description here

What've tried

struct Heart: View {
  var body: some View {
    MovableCard("heart.fill")
      .onTapGesture {
        // Notify this card is tapped
        NotificationCenter.default.post(heartOnTap, ["self":self])
      }
  }
}
struct BoardView: View {
  @StateObject var viewModel = ViewModel()
  var body: some View {
    ZStack{
      Heart()
      Heart()
      Path{ path in
        path.move(to: viewModel.firstCard.POS ?? CGPoint(x: -1, y: -1))
        path.addLine(to: viewModel.secondCard.POS ?? CGPoint(x: -1, y: -1))
      }
    }
  }
}
class ViewModel: ObservableObject {
  @Published var firstCard: Heart?
  @Published var secondCard: Heart?

  init() {
    NotificationCenter.default.addObserver(self, 
      selector: #selector(self.didTap(_:)), 
      name: heartOnTap, 
      object: nil)
  }

  @objc func didTap(_ notification: NSNotification) {
    guard let heart = notification.userInfo?["self"] as? Heart else { return }
    if firstCard == nil {
      firstCard = HEARTPOSITION // <- Heart position for drawing the line
    }
    if secondCard == nil {
      secondCard = HEARTPOSITION // <- Heart position for drawing the line
    }
  }
}

Discussion

Is there a possible solution for drawing line from view to another view. (view is movable)

Alternative Goal

DigicalSim (iOS)

enter image description here

Upvotes: 1

Views: 1136

Answers (1)

jnpdx
jnpdx

Reputation: 52387

The first thing about your initial plan that is potentially problematic is that you're trying to store references to Views. In general, this shouldn't be done in SwiftUI as Views are meant to be transient. Instead, I'd suggest creating an identifiable model that you can use to keep track of each View.

The View's position can be sent back up the View hierarchy using a PreferenceKey. Then, a Path can be drawn from the recorded positions in the parent view.

There are lots of little details here (like using ForEach for the heart views, the initial positions, etc) that are all changeable for your own circumstances. The important part is transmitting the positions up the hierarchy and then drawing a path based on those positions.

struct HeartModel : Identifiable {
    var id = UUID()
}

struct HeartView : View {
    var model : HeartModel
    @State private var offset = CGSize(width: 100, height: 100)
    @State private var initialDragPosition = CGSize(width: 100, height: 100)
    
    var body: some View {
        VStack {
            Image(systemName: "heart.fill")
                .resizable()
                .frame(width: 50, height: 50)
                .position(x: offset.width, y: offset.height)
                .preference(key: ViewPositionKey.self, value: [model.id:offset])
                .gesture(
                    DragGesture()
                        .onChanged { gesture in
                            self.offset = CGSize(width: initialDragPosition.width + gesture.translation.width, height: initialDragPosition.height + gesture.translation.height)
                        }
                        .onEnded { _ in
                            initialDragPosition = self.offset
                        }
                )
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView : View {
    @State private var heartModels : [HeartModel] = [.init(), .init()]
    @State private var positions : [UUID:CGSize] = [:]
    
    var body: some View {
        ZStack {
            ForEach(heartModels) { heart in
                HeartView(model: heart)
            }
            
            Path { path in
                if let first = positions.first {
                    path.move(to: CGPoint(x: first.value.width, y: first.value.height))
                }
                positions.forEach { item in
                    path.addLine(to: CGPoint(x: item.value.width, y: item.value.height))
                }
            }.stroke()
            
        }.onPreferenceChange(ViewPositionKey.self) { newPositions in
            positions = newPositions
        }
    }
}

struct ViewPositionKey: PreferenceKey {
    static var defaultValue: [UUID:CGSize] { [:] }
    static func reduce(value: inout [UUID:CGSize], nextValue: () -> [UUID:CGSize]) {
        let next = nextValue()
        if let item = next.first {
            value[item.key] = item.value
        }
    }
}

enter image description here

Upvotes: 2

Related Questions