I'm updating this question with new and more complete code, to show how I've attempted to implement the suggestion in the answer below from @HunterLion. Here's the original statement of the problem:
I am implementing a version of Pentominos using SwiftUI. When I drag a piece (view) onto the board, I'd like it to appear in front of other pieces (views) while being dragged, but it appears behind other pieces which were rendered later in the layout. When I drag the first piece (the U), it drags behind other pieces as well as the board:
When dropped, the piece positions itself in front as desired:
Per @HunterLion's suggestion, I have attempted to implement this using a @Published variable to set the zIndex in GameView, but it still doesn't work.
Regarding the following code, I haven't tried yet to create a minimum reproducible example -- not sure that's even possible, so this code is incomplete and not executable, but I think it shows the structure and relationships adequately.
GameView lays out the game space which contains the HomeViews and the board (image and BoardView). Each HomeView contains a PieceView which presents the individual pieces in their home positions. When a PieceView is dragged and dropped onto the board, it is redrawn within the BoardView (not shown).
The Pieces class contains a dictionary of the pieces, and this is where I put @Published var somethingsBeingDragged: Bool = false. somethingsBeingDragged is set in PieceView at the point where it is determined that a drag onto the board is occurring (as opposed to a shorter drag within PieceView that indicates a horizontal or vertical flip of the piece).
// GameView places the pieces and the board in the game space.
//
struct GameView: View {
var dropTarget = Target()
var map = Map(rows: constants.boardRows, cols: constants.boardCols)
@ObservedObject var homes: Homes
@ObservedObject var pieces: Pieces
var body: some View {
HStack
{
VStack {
homes.home["U"].modifier(smallPieceFrame())
homes.home["W"].modifier(smallPieceFrame())
homes.home["X"].modifier(smallPieceFrame())
homes.home["Y"].modifier(bigPieceFrame())
homes.home["I"].modifier(bigPieceFrame())
}
VStack {
homes.home["Z"].modifier(smallPieceFrame())
ZStack {
Image("board")
BoardView(rows: constants.boardRows, cols: constants.boardCols)
}
.zIndex(pieces.somethingsBeingDragged ? -1 : 1)
homes.home["V"].modifier(bigPieceFrame())
}
VStack {
homes.home["F"].modifier(smallPieceFrame())
homes.home["P"].modifier(smallPieceFrame())
homes.home["T"].modifier(smallPieceFrame())
homes.home["L"].modifier(bigPieceFrame())
homes.home["N"].modifier(bigPieceFrame())
}
}
...
----------------------------
// HomeView is the starting location of each piece, the location
// to which it returns if dropped illegally or removed from the board,
// and the location of the anchor image that remains after a
// piece is placed on the board.
//
struct HomeView: View {
var id: String // piece being displayed
var onBoard: Bool
@EnvironmentObject var pieces: Pieces
var body: some View {
ZStack {
PieceView(id: id, orientation: 8) // 8 => anchor image
if !onBoard {
PieceView(id: id, orientation: pieces.piece[id]!.orientation)
}
}
}
}
----------------------------
// PieceView tracks the individual game pieces, enables their
// reorientation by rotation (right and left) and reflection
// (horizontal and vertical) by gestures, enables their placement
// on the board by dragging.
//
struct PieceView: View {
var id: String // Identifies the piece
@State var dragOffset = CGSize.zero // Offset of piece while dragging
@State var dragging = false // T => piece is being dragged
@State var orientation: Int // orientation of image
@EnvironmentObject var dropTarget: Target
@EnvironmentObject var map: Map
@EnvironmentObject var pieces: Pieces
...
var body: some View {
Image(id + "\(orientation)")
.padding(0)
// .border(Color.gray)
.gesture(tapSingle)
.highPriorityGesture(tapDouble)
.offset(dragOffset)
.gesture(
DragGesture(coordinateSpace: .named("gameSpace"))
.onChanged { gesture in
dragging = false
pieces.somethingsBeingDragged = false
// Currently checking for drag by distance, but intend to change this.
//
if abs(Int(gesture.translation.width)) > Int(constants.dragTolerance) ||
abs(Int(gesture.translation.height)) > Int(constants.dragTolerance) {
dragOffset = gesture.translation
dragging = true
pieces.somethingsBeingDragged = true
}
}
.onEnded { gesture in
if dragging {
if onBoard(location: gesture.location) {
// piece has been legally dropped on board
//
dropTarget.pieceId = id
orientation = pieces.piece[id]!.orientation
} else {
// piece was dropped but not in a legal position, so goes home
//
dragOffset = CGSize(width: 0.0, height: 0.0)
}
} else {
// If not dragging, check for reflection.
//
...
}
}
}
)
.zIndex(dragging ? 1 : 0)
}
----------------------------
// Piece contains the state information about each piece: its size (in squares)
// and its current orientation.
//
class Piece: ObservableObject {
var orientation: Int = 0
let size: Int
init(size: Int) {
self.size = size
}
}
// Pieces contains the dictionary of Pieces.
//
class Pieces: ObservableObject {
@Published var somethingsBeingDragged: Bool = false
var piece: [String: Piece] = [:]
init() {
for name in smallPieceNames {
piece[name] = Piece(size: constants.smallPieceSquares)
}
for name in bigPieceNames {
piece[name] = Piece(size: constants.bigPieceSquares)
}
}
}
I'll appreciate any help on this.
PS @HunterLion, in answer to your "By the way" comment, I set dragging to true within the if statement because only drags of a certain minimal distance are interpreted as moves toward the game board. Shorter drags are interpreted to flip a piece vertically or horizontally. I intend to change how different drags are recognized, but this is it for now.


I have almost exactly the same code and it works perfectly with
.zIndex()(I assumedraggingis a@Statevariable in your view).But that's not enough: you need to move the board to the background when a piece is being dragged.
So, the solution is to have a
@Publishedvariable in your view model that changes together with (or instead of)dragging. If we cal that variableisSomethingBeingDragged, you can add another.zIndex()to the board, like this:If you prefer, instead of a variable in the view model, you can also use a
@Bindingbetween the two views.By the way: why don't you just move
dragging = trueout of theif{}condition? It should be the first line inside the.onChanged.Edit
After you have changed your question, I created the minimal reproducible example here below.
It was not working in your case because the pieces were still embedded in their
VStacks: while the.zIndex()of the piece is 1, the.zIndex()of theVStackis still 0. So, the piece goes to the front inside the stack, but the stack is still in the back.I just added more
.zIndex()modifiers and, as you can see from the code below, it works: the green letters get in the front while moving, the grid is in the front otherwise. Downside: all letters of theVStackget in the front at the same time.Try it as it is:
GameView, place the.zIndex()on the stacks:PieceView, remember to bring thedraggingvariable back to false when the gesture ends.