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