В моей модели есть объект, который пользователь может перетаскивать по экрану.
struct ScreenElement: Hashable {
var id = UUID()
var offset = CGSize.zero
var lastDragAmount = CGSize.zero
func hash(into myhasher: inout Hasher) {
myhasher.combine(id)
}
}
Модель содержит массив этих объектов:
@Observable
class DraggerModel {
var screenElements: [ScreenElement] = []
func addScreenElement() {
let newOne: ScreenElement = ScreenElement()
screenElements.append(newOne)
}
В моем представлении я размещаю объекты, а затем отслеживаю, когда пользователь перетаскивает любой из них:
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)")
}
)
}
Это работает! Когда я перетаскиваю объект, я получаю поток печатного вывода. Но я хочу на самом деле переместить объект, поэтому добавляю следующую функцию:
// 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
}
}
}
И я меняю функцию DragGesture для вызова этой функции:
.gesture(
DragGesture()
.onChanged { gesture in
print("new drag: \(element.id)")
myModel.changeDragAmount(element: element, theValue: gesture)
}
)
Этот метод (который отлично работает, когда есть только один объект, и он не находится в массиве) в данном случае не работает. Я получаю одно уведомление о перетаскивании, и все.
Я считаю, что это связано с тем, что в моей функции changeDragAmount
я каждый раз создаю совершенно новый массив (неизменяемость!), И новый массив не отслеживается и не получает информации о последующих уведомлениях о перетаскивании.
Как лучше это сделать? Это работает?
Спасибо @workingdogsupportUkraine, я изменил название модели, чтобы оно совпадало (ошибка вырезания и вставки). Но это почти весь код, который вызывает проблему, мне не хватает только некоторых закрывающих скобок здесь и там.
Попробуйте этот подход, используя struct ScreenElement: Identifiable
,
и модифицированный func changeDragAmount
. Обратите также внимание на ForEach(theModel.screenElements)
, нет id: \.self
.
Обратите внимание: вам придется отрегулировать положение (x,y) кругов в соответствии с вашими требованиями.
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)
}
)
}
}
}
}
РЕДАКТИРОВАТЬ-1:
Альтернативный подход к перетаскиванию массива кругов по представлению:
используя GeometryReader
и простой var position: CGPoint
.
Обратите внимание, что вам не нужен @Observable class DraggerModel
для этого достаточно простого @State private var screenElements: [ScreenElement]
.
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))
}
}
}
Да, ключевым моментом здесь было использование Идентифицируемого протокола; с его помощью мне не нужно было изменять функцию changeDragAmount
. Это привело меня к тонким различиям между Identible и Hashable, о которых я раньше не задумывался — я думал, что они могут быть взаимозаменяемыми. И мне нравится идея GeometryReader
, но моя структура будет намного сложнее, так что не в этот раз. Спасибо!
Я бы также предложил сделать
struct ScreenElement: Identifiable,...
и использовать@State private var theModel = DraggerModel()
в своемContentView
. Покажите полный пример кода, который на самом деле представляет вашу проблему.