0

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.

Card Stack

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!

7
  • Is CardHand a subView or mainView? Commented Mar 8, 2021 at 16:06
  • what is Card code? Commented Mar 8, 2021 at 16:11
  • CardHand is a SubView of the whole Game Scene. @swiftPunk do you mean Card or GameCard? I'd be happy to add it. Commented Mar 8, 2021 at 16:14
  • your code does not compile, you need provide working code, down voted for that reason. Commented Mar 8, 2021 at 16:16
  • I've added my model code. With all this code the project immediately compiles. Commented Mar 8, 2021 at 16:24

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.