简体   繁体   中英

Change array sequence in SwiftUI DragGesture

Im currently trying to build a deck of cards to represent the users cards.

This is my first prototype, please ignore the styling issues. I want the user to be able to change the sequence of the cards, which i tried to implement with a drag gesture.

卡叠

My idea was that when the drag gesture's translation is bigger than the cards offset (or smaller than the cards negative offset) I just swap the two elements inside my array to swap the cards in the view (and viewModel because of the binding). Unfortunately that's not working and I cannot figure out why. This is my View:

struct CardHand: View {
    @Binding var cards: [Card]
    @State var activeCardIndex: Int?
    @State var activeCardOffset: CGSize = CGSize.zero
    
    var body: some View {
        ZStack {
            ForEach(cards.indices) { index in
                GameCard(card: cards[index])
                    .offset(x: CGFloat(-30 * index), y: self.activeCardIndex == index ? -20 : 0)
                    .offset(activeCardIndex == index ? activeCardOffset : CGSize.zero)
                    .rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
                    .zIndex(activeCardIndex == index ? 100 : Double(index))
                    .gesture(getDragGesture(for: index))
            }
            // Move the stack back to the center of the screen
            .offset(x: CGFloat(12 * cards.count), y: 0)
        }
    }
    
    private func getDragGesture(for index: Int) -> some Gesture {
        DragGesture()
            .onChanged { gesture in
                self.activeCardIndex = index
                self.activeCardOffset = gesture.translation
            }
            .onEnded { gesture in
                if gesture.translation.width > 30, index < cards.count {
                    cards.swapAt(index, index + 1)
                }
                self.activeCardIndex = nil
                // DEBUG: Try removing the card after drag gesture ended. Not working either.
                cards.remove(at: index)
            }
    }
}

The GameCard:

struct GameCard: View {
    let card: Card
    var symbol: (String, Color) {
        switch card.suit.name {
            case "diamonds":
                return ("♦", .red)
            case "hearts":
                return ("♥", .red)
            case "spades":
                return ("♠", .black)
            case "clubs":
                return ("♣", .black)
            default:
                return ("none", .black)
        }
    }
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 25)
                .fill(Color.white)
                .addBorder(Color.black, width: 3, cornerRadius: 25)
            VStack {
                Text(self.card.rank.type.description())
                Text(self.symbol.0)
                    .font(.system(size: 100))
                    .foregroundColor(self.symbol.1)
            }
        }
        .frame(minWidth: 20, idealWidth: 80, maxWidth: 80, minHeight: 100, idealHeight: 100, maxHeight: 100, alignment: .center)
    }
}

The Model behind it:

public struct Card: Identifiable, Hashable {
    public var id: UUID = UUID()
    public var suit: Suit
    public var rank: Rank
}

public enum Type: Identifiable, Hashable {
    case face(String)
    case numeric(rankNumber: Int)
    
    public var id: Type { self }
    
    public func description() -> String{
        switch self {
        case .face(let description):
            return description
        case .numeric(let rankNumber):
            return String(rankNumber)
        }
    }
}

public struct Rank: RankProtocol, Identifiable, Hashable {
    public var id: UUID = UUID()
    public var type: Type
    public var value: Int
    public var ranking: Int
}

public struct Suit: SuitProtocol, Identifiable, Hashable {
    public var id: UUID = UUID()
    public var name: String
    public var value: Int
    
    public init(name: String, value: Int) {
        self.name = name
        self.value = value
    }
}

public protocol RankProtocol {
    var type: Type { get }
    var value: Int { get }
}

public protocol SuitProtocol {
    var name: String { get }
}

Can anyone tell me why this is not working as I expected? Did I miss anything basic?

Thank you!

Lots of little bugs going on here. Here's a (rough) solution, which I'll detail below:


struct CardHand: View {
    @Binding var cards: [Card]
    @State var activeCardIndex: Int?
    @State var activeCardOffset: CGSize = CGSize.zero
    
    func offsetForCardIndex(index: Int) -> CGSize {
        var initialOffset = CGSize(width: CGFloat(-30 * index), height: 0)
        guard index == activeCardIndex else {
            return initialOffset
        }
        initialOffset.width += activeCardOffset.width
        initialOffset.height += activeCardOffset.height
        return initialOffset
    }
    
    var body: some View {
        ZStack {
            ForEach(cards.indices) { index in
                GameCard(card: cards[index])
                    .offset(offsetForCardIndex(index: index))
                    .rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
                    .zIndex(activeCardIndex == index ? 100 : Double(index))
                    .gesture(getDragGesture(for: index))
            }
            // Move the stack back to the center of the screen
            .offset(x: CGFloat(12 * cards.count), y: 0)
        }
    }
    
    private func getDragGesture(for index: Int) -> some Gesture {
        DragGesture(minimumDistance: 0, coordinateSpace: .local)
            .onChanged { gesture in
                self.activeCardIndex = index
                self.activeCardOffset = gesture.translation
            }
            .onEnded { gesture in
                if gesture.translation.width > 30 {
                    if index - 1 > 0 {
                        cards.swapAt(index, index - 1)
                    }
                }
                if gesture.translation.width < -30 {
                    cards.swapAt(index, index + 1)
                }
                self.activeCardIndex = nil
            }
    }
}
  1. Trying to do two offset() calls in a row is problematic. I combined the two into offsetForCardIndex
  2. Because you're doing a ZStack , remember that the first item in the cards array will be at the bottom of the stack and towards the right of the screen. That affected the logic later
  3. You'll want to make sure you check the bounds of the array in swapAt so that you don't end up trying to swap beyond the indexes (which was what was happening before)

My "solution" is still pretty rough -- there should be more checking in place to make sure that the array can't go out of bounds. Also, in your original and in mine, it would probably make more sense from a UX perspective to be able to swap more than 1 position. But, that's a bigger issue outside the scope of this question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM