How can I rearrange a view's position in a ZStack by dragging it above or below another view (e.g. in this instance how can I rearrange the order of the cards in the deck by dragging a card above or below another card, to move the dragged card behind or in front of said card in deck).
I want for the card to change indices when dragged up or down in the stack and fluidly appear behind each and every card in the stack as it is dragged- and stay there on mouse up.
Summary: In other words, the card dragged and the cards above it should switch as I drag up and the card dragged and the cards below it should switch as I drag down.
I figure this has something to do with changing the ZStack order in struct CardView: View
and updating the position from inside DragGesture().onChanged
by evaluating how much the card has been dragged (perhaps by viewing the self.offset value) but I have not been able to work out how to do this in a reliable way.
Here's what I have right now:
Code:
import SwiftUI
let cardSpace:CGFloat = 10
struct ContentView: View {
@State var cardColors: [Color] = [.orange, .green, .yellow, .purple, .red, .orange, .green, .yellow, .purple]
var body: some View {
HStack {
VStack {
CardView(colors: self.$cardColors)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.position(x: 370, y: 300)
}
}
struct CardView: View {
@State var offset = CGSize.zero
@State var dragging:Bool = false
@State var tapped:Bool = false
@State var tappedLocation:Int = -1
@Binding var colors: [Color]
@State var locationDragged:Int = -1
var body: some View {
GeometryReader { reader in
ZStack {
ForEach(0..<self.colors.count, id: \.self) { i in
ColorCard(reader:reader, i:i, colors: self.$colors, offset: self.$offset, tappedLocation: self.$tappedLocation, locationDragged:self.$locationDragged, tapped: self.$tapped, dragging: self.$dragging)
}
}
}
.animation(.spring())
}
}
struct ColorCard: View {
var reader: GeometryProxy
var i:Int
@State var offsetHeightBeforeDragStarted: Int = 0
@Binding var colors: [Color]
@Binding var offset: CGSize
@Binding var tappedLocation:Int
@Binding var locationDragged:Int
@Binding var tapped:Bool
@Binding var dragging:Bool
var body: some View {
VStack {
Group {
VStack {
self.colors[i]
}
.frame(width: 300, height: 400)
.cornerRadius(20).shadow(radius: 20)
.offset(
x: (self.locationDragged == i) ? CGFloat(i) * self.offset.width / 14
: 0,
y: (self.locationDragged == i) ? CGFloat(i) * self.offset.height / 4
: 0
)
.offset(
x: (self.tapped && self.tappedLocation != i) ? 100 : 0,
y: (self.tapped && self.tappedLocation != i) ? 0 : 0
)
.position(x: reader.size.width / 2, y: (self.tapped && self.tappedLocation == i) ? -(cardSpace * CGFloat(i)) + 0 : reader.size.height / 2)
}
.rotationEffect(
(i % 2 == 0) ? .degrees(-0.2 * Double(arc4random_uniform(15)+1) ) : .degrees(0.2 * Double(arc4random_uniform(15)+1) )
)
.onTapGesture() { //Show the card
self.tapped.toggle()
self.tappedLocation = self.i
}
.gesture(
DragGesture()
.onChanged { gesture in
self.locationDragged = self.i
self.offset = gesture.translation
self.dragging = true
}
.onEnded { _ in
self.locationDragged = -1 //Reset
self.offset = .zero
self.dragging = false
self.tapped = false //enable drag to dismiss
self.offsetHeightBeforeDragStarted = Int(self.offset.height)
}
)
}.offset(y: (cardSpace * CGFloat(i)))
}
}
check this out:
the "trick" is that you just need to reorder the z order of the items. therefore you have to "hold" the cards in an array.
let cardSpace:CGFloat = 10
struct Card : Identifiable, Hashable, Equatable {
static func == (lhs: Card, rhs: Card) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var id = UUID()
var intID : Int
static let cardColors: [Color] = [.orange, .green, .yellow, .purple, .red, .orange, .green, .yellow, .purple]
var zIndex : Int
var color : Color
}
class Data: ObservableObject {
@Published var cards : [Card] = []
init() {
for i in 0..<Card.cardColors.count {
cards.append(Card(intID: i, zIndex: i, color: Card.cardColors[i]))
}
}
}
struct ContentView: View {
@State var data : Data = Data()
var body: some View {
HStack {
VStack {
CardView().environmentObject(data)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .position(x: 370, y: 300)
}
}
struct CardView: View {
@EnvironmentObject var data : Data
@State var offset = CGSize.zero
@State var dragging:Bool = false
@State var tapped:Bool = false
@State var tappedLocation:Int = -1
@State var locationDragged:Int = -1
var body: some View {
GeometryReader { reader in
ZStack {
ForEach(self.data.cards, id: \.self) { card in
ColorCard(card: card, reader:reader, offset: self.$offset, tappedLocation: self.$tappedLocation, locationDragged:self.$locationDragged, tapped: self.$tapped, dragging: self.$dragging)
.environmentObject(self.data)
.zIndex(Double(card.zIndex))
}
}
}
.animation(.spring())
}
}
struct ColorCard: View {
@EnvironmentObject var data : Data
var card: Card
var reader: GeometryProxy
@State var offsetHeightBeforeDragStarted: Int = 0
@Binding var offset: CGSize
@Binding var tappedLocation:Int
@Binding var locationDragged:Int
@Binding var tapped:Bool
@Binding var dragging:Bool
var body: some View {
VStack {
Group {
VStack {
card.color
}
.frame(width: 300, height: 400)
.cornerRadius(20).shadow(radius: 20)
.offset(
x: (self.locationDragged == card.intID) ? CGFloat(card.zIndex) * self.offset.width / 14
: 0,
y: (self.locationDragged == card.intID) ? CGFloat(card.zIndex) * self.offset.height / 4
: 0
)
.offset(
x: (self.tapped && self.tappedLocation != card.intID) ? 100 : 0,
y: (self.tapped && self.tappedLocation != card.intID) ? 0 : 0
)
.position(x: reader.size.width / 2, y: (self.tapped && self.tappedLocation == card.intID) ? -(cardSpace * CGFloat(card.zIndex)) + 0 : reader.size.height / 2)
}
.rotationEffect(
(card.zIndex % 2 == 0) ? .degrees(-0.2 * Double(arc4random_uniform(15)+1) ) : .degrees(0.2 * Double(arc4random_uniform(15)+1) )
)
.onTapGesture() { //Show the card
self.tapped.toggle()
self.tappedLocation = self.card.intID
}
.gesture(
DragGesture()
.onChanged { gesture in
self.locationDragged = self.card.intID
self.offset = gesture.translation
if self.offset.height > 60 ||
self.offset.height < -60 {
withAnimation {
if let index = self.data.cards.firstIndex(of: self.card) {
self.data.cards.remove(at: index)
self.data.cards.append(self.card)
for index in 0..<self.data.cards.count {
self.data.cards[index].zIndex = index
}
}
}
}
self.dragging = true
}
.onEnded { _ in
self.locationDragged = -1 //Reset
self.offset = .zero
self.dragging = false
self.tapped = false //enable drag to dismiss
self.offsetHeightBeforeDragStarted = Int(self.offset.height)
}
)
}.offset(y: (cardSpace * CGFloat(card.zIndex)))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Data())
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With