Reputation: 627
Heart
in BoardView
. Each heart has MovableCard
Heart
is tapped, then notify and pass selfstruct Heart: View {
var body: some View {
MovableCard("heart.fill")
.onTapGesture {
// Notify this card is tapped
NotificationCenter.default.post(heartOnTap, ["self":self])
}
}
}
heartOnTap
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))
}
}
}
}
ViewModel
will listen heartOnTap
for receiving Heart: View
Heart: View
not the tap position because it's movable.Heart
that conformed View
doesn't have any access for directly getting its positionclass 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
}
}
}
Is there a possible solution for drawing line from view to another view. (view is movable)
Upvotes: 1
Views: 1136
Reputation: 52387
The first thing about your initial plan that is potentially problematic is that you're trying to store references to View
s. In general, this shouldn't be done in SwiftUI as View
s 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
}
}
}
Upvotes: 2