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 HomeView
s 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.