SwiftUI перетаскивание объекта в массиве жестом

В моей модели есть объект, который пользователь может перетаскивать по экрану.

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 я каждый раз создаю совершенно новый массив (неизменяемость!), И новый массив не отслеживается и не получает информации о последующих уведомлениях о перетаскивании.

Как лучше это сделать? Это работает?

Я бы также предложил сделать struct ScreenElement: Identifiable,... и использовать @State private var theModel = DraggerModel() в своем ContentView. Покажите полный пример кода, который на самом деле представляет вашу проблему.

workingdog support Ukraine 06.09.2024 00:50

Спасибо @workingdogsupportUkraine, я изменил название модели, чтобы оно совпадало (ошибка вырезания и вставки). Но это почти весь код, который вызывает проблему, мне не хватает только некоторых закрывающих скобок здесь и там.

coco 06.09.2024 01:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Попробуйте этот подход, используя 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, но моя структура будет намного сложнее, так что не в этот раз. Спасибо!

coco 06.09.2024 17:21

Другие вопросы по теме