TokiTrainer/Toki Trainer/Views/FlashCardView.swift

302 lines
10 KiB
Swift
Raw Normal View History

2021-11-05 22:20:48 +00:00
//
// FlashCardView.swift
// Toki Trainer
//
// Created by Avery Ada Pace on 11/5/21.
//
import SwiftUI
import CoreData
2021-11-05 22:20:48 +00:00
2021-11-07 06:25:18 +00:00
enum FlashCardResult {
case Correct
case Incorrect
case Unanswered
}
2021-11-05 22:20:48 +00:00
struct FlashCardView: View {
@ObservedObject var flashCardsViewModel = FlashCardsViewModel()
var body: some View {
VStack {
FlashCardStack(dictionary: flashCardsViewModel.randomDictionary)
}
}
}
2021-11-07 06:25:18 +00:00
extension Binding {
func onChange(_ handler: @escaping () -> ()) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler()
})
}
}
struct FlashCardStack: View {
@Environment(\.managedObjectContext) private var viewContext
2021-11-07 22:15:11 +00:00
@FetchRequest(entity:FlashCardAnswer.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \FlashCardAnswer.word, ascending: false)]) var flashCardAnswers: FetchedResults<FlashCardAnswer>
2021-11-06 20:17:43 +00:00
var dictionary: [TokiDictEntry]
@State private var flashCards: [FlashCard] = []
2021-11-06 20:17:43 +00:00
@State private var topFlashCard: FlashCard? = nil
@State private var flashCardStack: [FlashCard] = []
2021-11-07 06:25:18 +00:00
@State private var flashCardsAreInteractive: [Bool] = []
2021-11-06 20:17:43 +00:00
@State private var flashCardsVertOffset: [CGFloat] = []
2021-11-07 06:25:18 +00:00
@State private var flashCardsResults: [FlashCardResult] = []
@State private var fadeOutOverlay = false
2021-11-06 20:17:43 +00:00
@State private var currentFlashCard = 0
var body: some View {
VStack {
ZStack {
2021-11-06 20:17:43 +00:00
if(flashCards.count > 0) {
ForEach(flashCards.indices, id: \.self) { index in
flashCards[index]
.offset(x: 0, y: flashCardsVertOffset[index])
2021-11-07 06:25:18 +00:00
.zIndex(-(CGFloat(index * 100)))
}
}
}
.overlay(HStack {
Image(systemName: "arrow.backward")
Text("Incorrect")
Spacer()
Text("Correct")
Image(systemName: "arrow.right")
}.opacity(fadeOutOverlay ? 0.0 : 1.0), alignment: .top)
2021-11-06 20:17:43 +00:00
}
2021-11-07 06:25:18 +00:00
Spacer()
2021-11-06 20:17:43 +00:00
.onAppear {
2021-11-07 06:25:18 +00:00
initFlashCards()
}
}
2021-11-07 06:25:18 +00:00
func initFlashCards() {
flashCards = []
for index in dictionary.indices {
2021-11-07 06:25:18 +00:00
flashCardsAreInteractive.append(false)
flashCardsResults.append(FlashCardResult.Unanswered)
flashCards.append(FlashCard(isInteractive: $flashCardsAreInteractive[index], result: $flashCardsResults[index].onChange(cardAnswerReceived), dictionaryEntry: dictionary[index]))
2021-11-07 06:25:18 +00:00
flashCardsVertOffset.append(470)
}
if flashCards.count - currentFlashCard >= 3 {
flashCardsVertOffset[currentFlashCard + 1] = 410
flashCardsVertOffset[currentFlashCard + 2] = 440
flashCardsVertOffset[currentFlashCard + 3] = 470
} else if flashCards.count - currentFlashCard == 2 {
flashCardsVertOffset[currentFlashCard + 1] = 410
flashCardsVertOffset[currentFlashCard + 2] = 440
} else if flashCards.count - currentFlashCard == 1 {
flashCardsVertOffset[currentFlashCard + 1] = 410
}
2021-11-07 06:25:18 +00:00
flashCardsVertOffset[currentFlashCard] = 100
flashCardsAreInteractive[currentFlashCard] = true
}
2021-11-07 22:15:11 +00:00
func setFlashCardAnswersCoreData(_ correct: Bool) {
var cardInDatabase = false
for answer in flashCardAnswers {
print(answer.word)
if answer.word == dictionary[currentFlashCard].word {
cardInDatabase = true
answer.setValue((answer.triesCount + 1), forKey: "triesCount")
if correct {
answer.setValue((answer.correctCount + 1), forKey: "correctCount")
}
print("answer found in database")
}
}
if cardInDatabase == false {
let answer = FlashCardAnswer(context: viewContext)
answer.word = dictionary[currentFlashCard].word
answer.triesCount = 1
if correct {
answer.correctCount = 1
}
print("answer not found in database")
}
// for answer in flashCardAnswers {
// if answer.word == dictionary[currentFlashCard].word {
// flashCardAnswer.word = answer.word
// flashCardAnswer.triesCount = answer.triesCount + 1
// if correct {
// flashCardAnswer.correctCount = answer.correctCount + 1
// }
// }
// }
try? viewContext.save()
}
func cardAnswerReceived() {
if flashCardsResults[currentFlashCard] == FlashCardResult.Correct {
2021-11-07 22:15:11 +00:00
setFlashCardAnswersCoreData(true)
} else if flashCardsResults[currentFlashCard] == FlashCardResult.Incorrect {
2021-11-07 22:15:11 +00:00
setFlashCardAnswersCoreData(false)
} else {
return
}
nextFlashCard()
}
2021-11-06 20:17:43 +00:00
func nextFlashCard() {
2021-11-07 06:25:18 +00:00
currentFlashCard += 1
2021-11-06 20:17:43 +00:00
if(currentFlashCard > 0 ) {
flashCardsVertOffset[currentFlashCard - 1] = -1000
}
2021-11-07 06:25:18 +00:00
flashCardsVertOffset[currentFlashCard] = 100
flashCardsAreInteractive[currentFlashCard] = true
self.fadeOutOverlay = true
2021-11-07 06:25:18 +00:00
if flashCards.count - currentFlashCard >= 3 {
flashCardsVertOffset[currentFlashCard + 1] = 410
flashCardsVertOffset[currentFlashCard + 2] = 440
flashCardsVertOffset[currentFlashCard + 3] = 470
} else if flashCards.count - currentFlashCard == 2 {
flashCardsVertOffset[currentFlashCard + 1] = 410
flashCardsVertOffset[currentFlashCard + 2] = 440
} else if flashCards.count - currentFlashCard == 1 {
flashCardsVertOffset[currentFlashCard + 1] = 410
}
2021-11-06 20:17:43 +00:00
}
func setTopFlashCard(card: FlashCard?) {
if let safeCard = card {
2021-11-07 06:25:18 +00:00
self.topFlashCard?.isInteractive = false
2021-11-06 20:17:43 +00:00
self.topFlashCard = safeCard
2021-11-07 06:25:18 +00:00
self.topFlashCard?.isInteractive = true
2021-11-06 20:17:43 +00:00
}
}
}
struct FlashCard: View {
2021-11-05 22:20:48 +00:00
let screen = UIScreen.main.bounds
2021-11-06 00:23:33 +00:00
@State var isFaceDown = false
@State var rotationAngle: Double = 0
2021-11-07 06:25:18 +00:00
@Binding var isInteractive: Bool
@Binding var result: FlashCardResult
var dictionaryEntry: TokiDictEntry
2021-11-05 22:20:48 +00:00
2021-11-07 06:25:18 +00:00
@State private var dragAmount = CGFloat(0)
var drag: some Gesture {
DragGesture()
.onChanged { gesture in
if isInteractive {
self.dragAmount = gesture.translation.width
}
}
.onEnded { gesture in
withAnimation {
if isInteractive {
if self.dragAmount < -20 {
self.dragAmount = -500
self.result = FlashCardResult.Incorrect
} else if self.dragAmount > 20 {
self.dragAmount = 500
self.result = FlashCardResult.Correct
} else {
self.dragAmount = 0
2021-11-07 06:25:18 +00:00
}
}
}
}
}
2021-11-05 22:20:48 +00:00
var body: some View {
Text("")
.modifier(CardFlipModifier(isFaceDown: isFaceDown, frontText: dictionaryEntry.word, backText: concatenateDefinitions()))
2021-11-06 00:23:33 +00:00
.frame(width: 0.8 * screen.width, height: 200.0)
2021-11-07 06:25:18 +00:00
.offset(x: isFaceDown ? -dragAmount : dragAmount, y: abs(dragAmount) / 10)
.rotationEffect(.degrees(isFaceDown ? -(dragAmount / 50) : dragAmount / 50))
2021-11-06 00:23:33 +00:00
.font(.title)
.rotation3DEffect(self.isFaceDown ? Angle(degrees: 180) : Angle(degrees: 0), axis: (x: 0.0, y: 10.0, z: 0.0))
.animation(.default)
.onTapGesture {
2021-11-07 06:25:18 +00:00
if self.isInteractive == true {
self.isFaceDown.toggle()
}
2021-11-06 00:23:33 +00:00
}
2021-11-07 06:25:18 +00:00
.gesture(drag)
}
func concatenateDefinitions() -> String {
var result = String()
for definition in dictionaryEntry.definitions {
2021-11-07 06:25:18 +00:00
result.append(contentsOf: "\(definition.definition)\n")
}
return result
}
func setCanBeFlipped(_ input: Bool) {
2021-11-07 06:25:18 +00:00
self.isInteractive.toggle()
2021-11-05 22:20:48 +00:00
}
}
struct CardFlipModifier: AnimatableModifier {
2021-11-06 00:23:33 +00:00
var frontText: String
var backText: String
var isFaceDown: Bool
2021-11-05 22:20:48 +00:00
var rotationAngle: Double
2021-11-06 00:23:33 +00:00
init(isFaceDown: Bool, frontText: String, backText: String) {
rotationAngle = isFaceDown ? 180 : 0
self.isFaceDown = isFaceDown
self.frontText = frontText
self.backText = backText
2021-11-05 22:20:48 +00:00
}
var animatableData: Double {
get { rotationAngle }
set { rotationAngle = newValue }
}
func body(content: Content) -> some View {
2021-11-06 00:23:33 +00:00
return ZStack {
2021-11-05 22:20:48 +00:00
RoundedRectangle(cornerRadius: 20.0)
2021-11-07 22:52:36 +00:00
.fill(rotationAngle < 90 ? Color("CardColor") : Color("CardColorBack"))
2021-11-07 06:25:18 +00:00
.animation(.none, value: rotationAngle)
.overlay(
RoundedRectangle(cornerRadius: 20)
2021-11-07 22:52:36 +00:00
.stroke(Color("CardColorBack"), lineWidth: 5))
2021-11-07 06:25:18 +00:00
.animation(.none, value: rotationAngle)
2021-11-07 22:52:36 +00:00
Image("CardLogoDark")
.resizable()
.frame(width: 180, height: 180, alignment: .center)
.opacity(0.5)
2021-11-06 00:23:33 +00:00
Text(frontText)
.font(.title)
2021-11-07 22:52:36 +00:00
.foregroundColor(.white)
2021-11-05 22:20:48 +00:00
.opacity(rotationAngle < 90 ? 1.0 : 0.0)
2021-11-07 06:25:18 +00:00
.animation(.none, value: rotationAngle)
2021-11-06 00:23:33 +00:00
Text(backText)
.font(.subheadline)
.padding()
2021-11-07 22:52:36 +00:00
.foregroundColor(.white)
2021-11-06 00:23:33 +00:00
.opacity(rotationAngle < 90 ? 0.0 : 1.0)
2021-11-07 06:25:18 +00:00
.animation(.none, value: rotationAngle)
2021-11-06 00:23:33 +00:00
.scaleEffect(CGSize(width: -1.0, height: 1.0))
2021-11-05 22:20:48 +00:00
}
}
}
struct FlashCardView_Previews: PreviewProvider {
static var previews: some View {
FlashCardView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
2021-11-05 22:20:48 +00:00
}
}