In my model, I have an object which the user can drag around the screen.
struct ScreenElement: Hashable {
var id = UUID()
var offset = CGSize.zero
var lastDragAmount = CGSize.zero
func hash(into myhasher: inout Hasher) {
myhasher.combine(id)
}
}
The model holds an array of these objects:
@Observable
class DraggerModel {
var screenElements: [ScreenElement] = []
func addScreenElement() {
let newOne: ScreenElement = ScreenElement()
screenElements.append(newOne)
}
In my View, I place the objects, and then track when the user drags on any one of them:
struct ContentView: View {
@State private var theModel = DraggerModel()
var body: some View {
VStack {
ForEach(theModel.screenElements, id: \.self) { element in
Circle()
.foregroundColor(.red)
.frame(width: 200)
.offset(element.offset)
.gesture(
DragGesture()
.onChanged { gesture in
print("new drag: \(element.id)")
}
)
}
This works! When I drag on an object, I get a stream of printed output. But I want to actually move the object, so I add the following function:
// given an object, change its drag amount
func changeDragAmount(element: ScreenElement, theValue: DragGesture.Value) {
for index in screenElements.indices {
if screenElements[index].id == element.id {
screenElements[index].offset = screenElements[index].lastDragAmount + theValue.translation
}
}
}
And I change the DragGesture function to call this function:
.gesture(
DragGesture()
.onChanged { gesture in
print("new drag: \(element.id)")
myModel.changeDragAmount(element: element, theValue: gesture)
}
)
This technique (which works great when there is only one object, and it's not in an array), doesn't work in this case. I get a single Drag notification, and that's it.
I believe this is because in my changeDragAmount
function, I am creating a whole new array each time (immutability!), and the new array isn't watching for, or isn't being informed of the subsequent Drag notifications.
What is a better way to do this? That works?
Try this approach using struct ScreenElement: Identifiable
,
and the modified func changeDragAmount
. Note also the ForEach(theModel.screenElements)
, no id: \.self
.
Note, you will have to adjust the positioning (x,y) of the Circles to meet your requirements.
struct ScreenElement: Identifiable { //<--- here
let id = UUID()
var offset = CGSize.zero
var lastDragAmount = CGSize.zero
}
@Observable
class DraggerModel {
// --- for testing
var screenElements: [ScreenElement] = [ScreenElement(), ScreenElement(), ScreenElement()]
func addScreenElement() {
let newOne: ScreenElement = ScreenElement()
screenElements.append(newOne)
}
func changeDragAmount(element: ScreenElement, theValue: DragGesture.Value) {
for index in screenElements.indices {
if screenElements[index].id == element.id {
//--- here
screenElements[index].offset = CGSize(width: screenElements[index].lastDragAmount.width + theValue.translation.width, height: screenElements[index].lastDragAmount.height + theValue.translation.height)
}
}
}
}
struct ContentView: View {
@State private var theModel = DraggerModel()
var body: some View {
VStack {
ForEach(theModel.screenElements) { element in //<--- here
Circle()
.foregroundColor(.red)
.frame(width: 50)
.offset(element.offset)
.gesture(
DragGesture().onChanged { gesture in
theModel.changeDragAmount(element: element, theValue: gesture)
}
)
}
}
}
}
EDIT-1:
An alternative approach to drag an array of Circles around the view is
using GeometryReader
and a simple var position: CGPoint
.
Note you don't need a @Observable class DraggerModel
for that, a simple @State private var screenElements: [ScreenElement]
would suffice.
struct ScreenElement: Identifiable {
let id = UUID()
var position: CGPoint
}
@Observable
class DraggerModel {
var screenElements: [ScreenElement] = [
ScreenElement(position: CGPoint(x: 100, y: 100)),
ScreenElement(position: CGPoint(x: 200, y: 200)),
ScreenElement(position: CGPoint(x: 250, y: 250))
]
func addScreenElement() {
let newOne = ScreenElement(position: CGPoint(x: 100, y: 100))
screenElements.append(newOne)
}
}
struct ContentView: View {
@State private var theModel = DraggerModel()
var body: some View {
VStack {
Button("Add new") {
theModel.addScreenElement()
}.buttonStyle(.bordered)
GeometryReader { geometry in
ForEach($theModel.screenElements) { $element in
Circle()
.fill(Color.red)
.frame(width: 50, height: 50)
.position(element.position)
.gesture(
DragGesture()
.onChanged { value in
element.position = value.location
}
)
}
}
.background(Color.gray.opacity(0.2))
}
}
}