How to implement drag and drop of SF Symbols within a SwiftUI grid?

I'm working on a chess GUI app (my first app) and I can't seem to figure out how to implement the drop part of drag and drop. So far I'm able to display a static chess board and pieces onto the screen and either use onTapGesture or DragGesture to move them but the UX isn't right yet. Do I need to use the draggable and dropDestination methods instead? Any advice or a simple code example that would get me going would be appreciated. Here are my requirements below to be clear:

  • Display an 8x8 Grid View (or I suppose LazyGrid) where all cells contain a Color view, and some may overlay an SF Symbol Image depending on the models state
  • UI needs to update the underlying model state and vice versa
  • Drag and drop gesture should be self contained within the Grid View, so no transfer of data between other window instances
  • Only one SF Symbol should be draggable and droppable at a time
  • An SF symbol dropped onto another should remove it from the state model
  • The drop gesture should snap a piece into the centre of the closest grid cell (or return it if it was an illegal chess move)
  • No need for backwards compatibility, I'm focusing on current software and hardware
  • Compatible with iOS, MacOS, iPadOS, WatchOS and potentially VisionOS
Answered by ssmith_c in 756256022

the 'drop' part of a DragGesture can be handled by the DragGesture.onEnded closure. In your .onChanged closure you can figure out where the drag is currently (value.location), so you could highlight legal destinations. In your .onEnded closure you figure out whether the move was legal and update your model accordingly.

Your question is a bit broad in scope. Try to narrow it down to something where you can show a piece of self-contained code, and explain what you expect, and what actually happens.

Accepted Answer

the 'drop' part of a DragGesture can be handled by the DragGesture.onEnded closure. In your .onChanged closure you can figure out where the drag is currently (value.location), so you could highlight legal destinations. In your .onEnded closure you figure out whether the move was legal and update your model accordingly.

Your question is a bit broad in scope. Try to narrow it down to something where you can show a piece of self-contained code, and explain what you expect, and what actually happens.

Thank you for responding to my question. Below is a simplified code example that displays two SF Symbol images on a Grid View, as seen in the gif. I've included a basic DragGesture but I have a feeling it isn't the right approach. In order for it to work the onEnded closure would need to somehow map the offset coordinate into a Square enum case so that I could update the ModelState. I intend for the board and pieces to scale to fit the available space so the size of the squares won't be known beforehand, making the coordinate mapping difficult. Maybe **GeometryReader ** could solve that but I could see the code becoming a mess. I see other drag and drop methods in the documentation, surely one them is designed to better fit this use case. dropDestination seems like a suitable candidate but I think read somewhere that it doesn't work with SF Symbols. I'm looking for guidance from someone familiar with Apple's documentation because they provide little explanations themselves (I've watched WWDC videos). Also there appears to be a clipping issue with the DragGesture within the Grid, changing the zindex of the images didn't fix that for me.

enum Square: Int { case a, b, c, d }

struct PieceView: View {
    @State var offset: CGSize = .zero
    var drag: some Gesture {
            DragGesture()
            .onChanged { value in offset = value.translation }
            .onEnded{ value in offset = .zero }
        }
    var body: some View {
        Image(systemName: "circle.fill")
            .resizable().scaledToFit()
            .foregroundColor(.black)
            .offset(offset)
            .gesture(drag)
    }
}

struct SquareView: View {
    let color: Color
    let square: Square
    @ObservedObject var state: ModelState
    var body: some View {
        color.overlay(state.pieces[square])
    }
}

class ModelState: ObservableObject {
    @Published var pieces: [Square: PieceView] = [.a: PieceView(), .b: PieceView()]
}

struct BoardView: View {
    @StateObject var state: ModelState = ModelState()
    var body: some View {
        Grid {
            ForEach(0..<2, id: \.self) { row in
                GridRow {
                    ForEach(0..<2, id: \.self) { column in
                        SquareView(
                            color: (row + column).isMultiple(of: 2) ? Color(.cyan) : Color(.white),
                            square: Square(rawValue: 2*row + column)!,
                            state: state
                        )
                    }
                }
            }
        }
    }
}

I faced a similar issue recently.

First you have to set the z-index of the SquareView to be higher if it is selected. That is not enough though, as it will only be in front of SquareViews in the same GridRow.

That is because the z-index of nested views in SwiftUI does not propagate to parent views. So we must also set the z-index of that GridRow to be higher if it contains the piece being dragged.

Store the piece being dragged's id and use that for the comparison!

How to implement drag and drop of SF Symbols within a SwiftUI grid?
 
 
Q