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.
I have almost exactly the same code and it works perfectly with .zIndex()
(I assume dragging
is a @State
variable 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 @Published
variable in your view model that changes together with (or instead of) dragging
. If we cal that variable isSomethingBeingDragged
, you can add another .zIndex()
to the board, like this:
ZStack {
Image("board")
BoardView(rows: constants.boardRows, cols: constants.boardCols)
}
.zIndex(viewModel.isSomethingBeingDragged ? -1 : 1)
If you prefer, instead of a variable in the view model, you can also use a @Binding
between the two views.
By the way: why don't you just move dragging = true
out of the if{}
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 VStack
s: while the .zIndex()
of the piece is 1, the .zIndex()
of the VStack
is 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 the VStack
get in the front at the same time.
Try it as it is:
GameView
, place the .zIndex()
on the stacks:struct GameView: View {
@StateObject private var pieces = Pieces()
var body: some View {
HStack {
VStack {
PieceView(id: "A", orientation: 0, pieces: pieces)
PieceView(id: "B", orientation: 0, pieces: pieces)
PieceView(id: "C", orientation: 0, pieces: pieces)
}
.zIndex(pieces.somethingsBeingDragged ? 1 : 0)
VStack {
ZStack {
Image(systemName: "square.grid.3x3")
.font(.system(size: 200))
}
}
.zIndex(pieces.somethingsBeingDragged ? -1 : 1)
VStack {
PieceView(id: "X", orientation: 0, pieces: pieces)
PieceView(id: "Y", orientation: 0, pieces: pieces)
PieceView(id: "Z", orientation: 0, pieces: pieces)
}
.zIndex(pieces.somethingsBeingDragged ? 1 : 0)
}
}
}
PieceView
, remember to bring the dragging
variable back to false when the gesture ends.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
didSet {
pieces.somethingsBeingDragged = dragging
}
}
@State var orientation: Int // orientation of image
@ObservedObject var pieces: Pieces
var body: some View {
Text(id)
.font(.system(size: 100))
.fontWeight(.black)
.foregroundColor(.green)
.zIndex(dragging ? 1 : 0)
.padding(0)
.gesture(TapGesture())
.highPriorityGesture(TapGesture(count: 2))
.offset(dragOffset)
.gesture(
DragGesture(coordinateSpace: .named("gameSpace"))
.onChanged { gesture in
dragging = false
// Currently checking for drag by distance, but intend to change this.
//
if abs(Int(gesture.translation.width)) > Int(10) ||
abs(Int(gesture.translation.height)) > Int(10) {
dragOffset = gesture.translation
dragging = true
}
}
.onEnded { gesture in
if dragging {
if gesture.location.y < 300.0 {
// piece has been legally dropped on board
//
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)
}
}
// On ended: bring the variables back to false
dragging = false
}
)
}
}
struct Piece {
var orientation: Int = 0
let size: Int
init(size: Int) {
self.size = size
}
}
class Pieces: ObservableObject {
@Published var somethingsBeingDragged: Bool = false
var piece: [String: Piece] = [:]
let smallPieceNames = ["A", "B", "C"]
let bigPieceNames = ["X", "Y", "Z"]
init() {
for name in smallPieceNames {
piece[name] = Piece(size: 20)
}
for name in bigPieceNames {
piece[name] = Piece(size: 20)
}
}
}