Search code examples
swiftswiftuidraggable

How to rearrange views in SwiftUI ZStack by dragging


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:

drawing

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)))
    }
}

Solution

  • 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())
        }
    }
    

    enter image description here