UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 91 - View doesn't update when wrong cards added back to bottom of pile

Forums > 100 Days of SwiftUI

I've got a variable called isWrong for which I have a binding between ContentView and CardView. In CardView it sets the value of isWrong based on whether the user swipes left or right. If the card is marked wrong then the removeCard function in ContentView removes it from the cards array and reinserts it at the bottom in position 0. Otherwise the card is removed from the pile entirely. I can see that the array count decrements when a card is removed and I can see that the array maintains the same count if the card is marked wrong and reinserted. However, despite the cards array being managed correctly, the view doesn't reflect that. No cards appear at the bottom of the pile upon reinsertion and eventually the pile runs out except the end game state is never reached because the array isn't empty. Why aren't the cards showing up in the bottom of the pile in the UI?

ContentView:

import SwiftUI

struct ContentView: View {
    @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
    @Environment(\.scenePhase) var scenePhase
    @Environment(\.accessibilityVoiceOverEnabled) var voiceOverEnabled

    @State private var cards = [Card]()
    @State private var timeRemaining = 100
    @State private var isActive = true
    @State private var showingEditScreen = false
    @State var isWrong = false

    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        ZStack {
            Image(decorative: "background")
                .resizable()
                .ignoresSafeArea()
            VStack {
                Text("Time: \(timeRemaining)")
                    .font(.largeTitle)
                    .foregroundColor(.white)
                    .padding(.horizontal, 20)
                    .padding(.vertical, 5)
                    .background(.black.opacity(0.75))
                    .clipShape(Capsule())
                ZStack {
                    ForEach(cards, id: \.id) { card in
                        CardView(card: card, isWrong: $isWrong) {
                            withAnimation {
                                removeCard(at: getCardIndex(card: card), card: card)
                            }
                        }
                            .stacked(at: getCardIndex(card: card), in: cards.count)
                            .allowsHitTesting(getCardIndex(card: card) == cards.count - 1)
                            .accessibilityHidden(getCardIndex(card: card) < cards.count - 1)
                    }
                }
                .allowsHitTesting(timeRemaining > 0)
                if cards.isEmpty {
                    Button("Start Again", action: resetCards)
                        .padding()
                        .background(.white)
                        .foregroundColor(.black)
                        .clipShape(Capsule())
                }
            }

            VStack {
                HStack {
                    Spacer()

                    Button {
                        showingEditScreen = true
                    } label: {
                        Image(systemName: "plus.circle")
                            .padding()
                            .background(.black.opacity(0.7))
                            .clipShape(Circle())
                    }
                }
                Spacer()
            }
            .foregroundColor(.white)
            .font(.largeTitle)
            .padding()

            if differentiateWithoutColor || voiceOverEnabled {
                VStack {
                    Spacer()

                    HStack {
                        Button {
                            withAnimation {
                                removeCard(at: cards.count - 1, card: cards[cards.count - 1])
                            }
                        } label: {
                            Image(systemName: "xmark.circle")
                                .padding()
                                .background(.black.opacity(0.7))
                                .clipShape(Circle())
                        }
                        .accessibilityLabel("Wrong")
                        .accessibilityHint("Mark your answer as being incorrect.")

                        Spacer()

                        Button {
                            withAnimation {
                                removeCard(at: cards.count - 1, card: cards[cards.count - 1])
                            }
                        } label: {
                            Image(systemName: "checkmark.circle")
                                .padding()
                                .background(.black.opacity(0.7))
                                .clipShape(Circle())
                        }
                        .accessibilityLabel("Correct")
                        .accessibilityHint("Mark your answer as being correct.")
                    }
                    .foregroundColor(.white)
                    .font(.largeTitle)
                    .padding()
                }
            }
        }
        .sheet(isPresented: $showingEditScreen, onDismiss: resetCards) {
            EditCards()
        }
        .onAppear(perform: resetCards)
        .onReceive(timer) { time in
            guard isActive else { return }
            if timeRemaining > 0 {
                timeRemaining -= 1
            }
        }
        .onChange(of: scenePhase) { newPhase in
            if newPhase == .active {
                if cards.isEmpty == false {
                    isActive = true
                }
            } else {
                isActive = false
            }
        }
    }

    func removeCard(at index: Int, card: Card) {
        guard index >= 0 else { return }

        print("In removeCard, isWrong: \(isWrong)")

        cards.remove(at: index)

        if isWrong {
            let wrongCard = Card(id: card.id, prompt: card.prompt, answer: card.answer)
            cards.insert(wrongCard, at: 0)
        }

        if cards.isEmpty {
            isActive = false
        }

        print("Card count: \(cards.count)")
    }

    func resetCards() {
        timeRemaining = 100
        isActive = true
        loadData()
    }

    func loadData() {
        if let data = UserDefaults.standard.data(forKey: "Cards") {
            if let decoded = try? JSONDecoder().decode([Card].self, from: data) {
                cards = decoded
            }
        }
    }

    func getCardIndex(card: Card) -> Int {
        for i in 0...cards.count {
            if cards[i].id == card.id {
                return i
            }
        }

        return -1
    }
}

extension View {
    func stacked(at position: Int, in total: Int) -> some View {
        let offset = Double(total - position)
        return self.offset(x: 0, y: offset * 10)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

CardView:

import SwiftUI

struct CardView: View {
    @State var card: Card
    @Binding var isWrong: Bool
    var removal: (() -> Void)? = nil

    @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
    @Environment(\.accessibilityVoiceOverEnabled) var voiceOverEnabled

    @State private var isShowingAnswer = false
    @State private var offset = CGSize.zero
    @State private var feedback = UINotificationFeedbackGenerator()

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 25, style: .continuous)
                .fill(
                    differentiateWithoutColor
                    ? .white
                    : .white.opacity(1 - Double(abs(offset.width / 50)))
                )
                .background(
                    differentiateWithoutColor
                    ? nil
                    : RoundedRectangle(cornerRadius: 25, style: .continuous)
                        .fill(getOffsetColor())
                    )
                .shadow(radius: 10)

            VStack {
                if voiceOverEnabled {
                    Text(isShowingAnswer ? card.answer : card.prompt)
                        .font(.largeTitle)
                        .foregroundColor(.black)
                } else {
                    Text(card.prompt)
                        .font(.largeTitle)
                        .foregroundColor(.black)

                    if isShowingAnswer {
                        Text(card.answer)
                            .font(.title)
                            .foregroundColor(.gray)
                    }
                }
            }
            .padding(20)
            .multilineTextAlignment(.center)
        }
        .frame(width: 450, height: 250)
        .rotationEffect(.degrees(Double(offset.width / 5)))
        .offset(x: offset.width * 5, y: 0)
        .opacity(2 - Double(abs(offset.width / 50)))
        .accessibilityAddTraits(.isButton)
        .gesture(DragGesture()
            .onChanged { gesture in
                offset = gesture.translation
                feedback.prepare()
            }
            .onEnded { _ in
                if abs(offset.width) > 100 {
                    if offset.width > 0 {
                        print("in card is correct branch")
                        feedback.notificationOccurred(.success)
                        isWrong = false

                    } else {
                        print("in card is wrong branch")
                        feedback.notificationOccurred(.error)
                        isWrong = true
                        print("isWrong value: \(isWrong)")
                    }
                    removal?()
                } else {
                    offset = .zero
                }
            })
        .onTapGesture {
            isShowingAnswer.toggle()
        }
        .animation(.spring(), value: offset)
    }

    func getOffsetColor() -> Color {
        if offset == CGSize.zero {
            return .white
        }

        if offset.width > 0 {
            return .green
        } else {
            return .red
        }
    }
}

2      

@sly3  

Hi @Legion,

The way you have things set up works apart from one small bug in the removeCard function.

When messing with that function, I was running into id collisions as I reinserted the incorrect card back into the pile - Xcode thought there were 2 of the same card in the pile at the same time (swiping it away to the left, and in the card pile still) since the card's id remains the same. So I simply give the incorrect card a new UUID when reinserting it in it's object creation stage:

        if isWrong {
            let wrongCard = Card(id: UUID(), prompt: card.prompt, answer: card.answer)
            cards.insert(wrongCard, at: 0)
        }

This works assuming you already made your Card struct conform to Identifiable which it looks like you did in the ForEach loop.
Deleting is unaffected as well since the value of the id itself doesn't matter, as long as it's unique so that it can be deleted from the other cards when swiped to the right.

Hope it helps.

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.