From 7b7717afe8a5e0d12858ae7f4f22b5258bbf8be9 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 8 Aug 2025 22:33:49 +0200 Subject: [PATCH] feat: new file structure --- QuizApp/Models/QuizQuestion.swift | 51 ++++++ .../{data.json => Resources/questions.json} | 0 QuizApp/{ => ViewModels}/ViewModel.swift | 13 +- .../{ => Views}/CategorySelectionView.swift | 0 QuizApp/{ => Views}/ContentView.swift | 164 ++++++++---------- QuizApp/Views/EstimationQuestionView.swift | 99 +++++++++++ .../Views/MultipleChoiceQuestionView.swift | 66 +++++++ QuizApp/Views/QuizFinishedView.swift | 50 ++++++ QuizApp/Views/QuizHeaderView.swift | 36 ++++ 9 files changed, 386 insertions(+), 93 deletions(-) create mode 100644 QuizApp/Models/QuizQuestion.swift rename QuizApp/{data.json => Resources/questions.json} (100%) rename QuizApp/{ => ViewModels}/ViewModel.swift (87%) rename QuizApp/{ => Views}/CategorySelectionView.swift (100%) rename QuizApp/{ => Views}/ContentView.swift (56%) create mode 100644 QuizApp/Views/EstimationQuestionView.swift create mode 100644 QuizApp/Views/MultipleChoiceQuestionView.swift create mode 100644 QuizApp/Views/QuizFinishedView.swift create mode 100644 QuizApp/Views/QuizHeaderView.swift diff --git a/QuizApp/Models/QuizQuestion.swift b/QuizApp/Models/QuizQuestion.swift new file mode 100644 index 0000000..a971bc3 --- /dev/null +++ b/QuizApp/Models/QuizQuestion.swift @@ -0,0 +1,51 @@ +// +// Topic.swift +// QuizApp +// +// Created by Paul on 03.08.25. +// + +import Foundation + +struct QuizQuestion: Decodable, Hashable, Identifiable { + let id: UUID + let question: String + let answers: [String] + let correctAnswer: Int + + let type: String? + let minValue: Double? + let maxValue: Double? + let correctValue: Double? + let unit: String? + + // Manuelles Decoding, id wird generiert + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + question = try container.decode(String.self, forKey: .question) + + // Falls im JSON nicht vorhanden, Standard setzen + answers = (try? container.decode([String].self, forKey: .answers)) ?? [] + correctAnswer = (try? container.decode(Int.self, forKey: .correctAnswer)) ?? 0 + + // Optional + type = try container.decodeIfPresent(String.self, forKey: .type) + minValue = try container.decodeIfPresent(Double.self, forKey: .minValue) + maxValue = try container.decodeIfPresent(Double.self, forKey: .maxValue) + correctValue = try container.decodeIfPresent(Double.self, forKey: .correctValue) + unit = try container.decodeIfPresent(String.self, forKey: .unit) + + id = UUID() + } + + private enum CodingKeys: String, CodingKey { + case question, answers, correctAnswer + case type, minValue, maxValue, correctValue, unit + } + + var isEstimation: Bool { + if let t = type?.lowercased() { return t == "estimation" } + return !answers.isEmpty ? false : (minValue != nil || maxValue != nil || correctValue != nil) + } +} diff --git a/QuizApp/data.json b/QuizApp/Resources/questions.json similarity index 100% rename from QuizApp/data.json rename to QuizApp/Resources/questions.json diff --git a/QuizApp/ViewModel.swift b/QuizApp/ViewModels/ViewModel.swift similarity index 87% rename from QuizApp/ViewModel.swift rename to QuizApp/ViewModels/ViewModel.swift index 3b32bc4..b749568 100644 --- a/QuizApp/ViewModel.swift +++ b/QuizApp/ViewModels/ViewModel.swift @@ -25,17 +25,17 @@ class ViewModel: ObservableObject { } var currentQuestion: QuizQuestion? { - guard currentQuestionIndex < questions.count else { return nil } - return questions[currentQuestionIndex] - } + guard currentQuestionIndex < questions.count else { return nil } + return questions[currentQuestionIndex] + } var availableCategories: [String] { return Array(allQuestionsByCategory.keys).sorted() } - + init() { - guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else { + guard let url = Bundle.main.url(forResource: "questions", withExtension: "json") else { questions = [] return } @@ -43,6 +43,7 @@ class ViewModel: ObservableObject { do { let data = try Data(contentsOf: url) allQuestionsByCategory = try JSONDecoder().decode([String: [QuizQuestion]].self, from: data) + print("Fragen geladen") } catch { print("Fehler beim Laden des Inhalts: \(error)") allQuestionsByCategory = [:] @@ -66,7 +67,7 @@ class ViewModel: ObservableObject { answeredCount = 0 selectedAnswers = Array(repeating: nil, count: questions.count) } - + func incrementScore(selectedIndex: Int) { if let correct = currentQuestion?.correctAnswer, selectedIndex == correct { diff --git a/QuizApp/CategorySelectionView.swift b/QuizApp/Views/CategorySelectionView.swift similarity index 100% rename from QuizApp/CategorySelectionView.swift rename to QuizApp/Views/CategorySelectionView.swift diff --git a/QuizApp/ContentView.swift b/QuizApp/Views/ContentView.swift similarity index 56% rename from QuizApp/ContentView.swift rename to QuizApp/Views/ContentView.swift index c0bb598..828a6ab 100644 --- a/QuizApp/ContentView.swift +++ b/QuizApp/Views/ContentView.swift @@ -20,115 +20,70 @@ struct ContentView: View { @State private var playerName: String = "" @State private var scoreIsSaved = false + @State private var estimationValue: Double = 0 + @State private var estimationSubmitted: Bool = false + @State private var estimationPoints: Int = 0 + var body: some View { VStack { if viewModel.isQuizFinished { - VStack(spacing: 20) { - Text("🎉 Quiz beendet!") - .font(.title) - - Text("Dein Ergebnis: \(viewModel.score) von \(viewModel.questions.count) Punkten") - .font(.headline) - - Button("Nochmal spielen") { - restartQuiz() - } - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(10) - - Button("Zurück zur Kategorieauswahl") { - dismiss() - - } - .padding() - .background(Color.gray) - .foregroundColor(.white) - .cornerRadius(10) - - TextField("Dein Name:", text: $playerName) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding(.horizontal) - - Button("Score speichern") { + QuizFinishedView( + score: viewModel.score, + total: viewModel.questions.count, + playerName: $playerName, + onPlayAgain: { restartQuiz() }, + onBackToCategories: { dismiss() }, + onSaveScore: { saveScore() dismiss() } - .padding() - .background(playerName.isEmpty ? Color.gray : Color.green) - .foregroundColor(.white) - .cornerRadius(10) - .disabled(playerName.isEmpty) - } + ) .onAppear { AudioServicesPlaySystemSound(1322) score += viewModel.score } } else if let frage = viewModel.currentQuestion { - Text("Punkte: \(viewModel.score)") - .font(.headline) - .padding(.bottom, 10) - //Fortschrittsbalken - VStack(spacing: 4) { - HStack(spacing: 4) { - ForEach(0.. Int { + let span = maxValue - minValue + guard span > 0 else { return 0 } + let absError = abs(guess - correct) + let relError = absError / span + if relError >= thresholdPercent { return 0 } + let ratio = 1.0 - (relError / thresholdPercent) + return max(0, Int(round(Double(maxPoints) * ratio))) + } + } #Preview { diff --git a/QuizApp/Views/EstimationQuestionView.swift b/QuizApp/Views/EstimationQuestionView.swift new file mode 100644 index 0000000..cf19f0f --- /dev/null +++ b/QuizApp/Views/EstimationQuestionView.swift @@ -0,0 +1,99 @@ +// +// EstimationQuestionView.swift +// QuizApp +// +// Created by Paul on 08.08.25. +// + +import SwiftUI + +struct EstimationQuestionView: View { + let minValue: Double + let maxValue: Double + let correctValue: Double + let unit: String + + @Binding var value: Double + @Binding var submitted: Bool + @Binding var gainedPoints: Int + let onSubmit: (Int) -> Void + + var body: some View { + VStack(spacing: 16) { + Slider( + value: Binding( + get: { value }, + set: { value = min(max($0, minValue), maxValue) } // clamp + ), + in: minValue...maxValue, + step: (maxValue - minValue) > 200 ? 1 : 0.5 + ) + .onAppear { + if value < minValue || value > maxValue { + value = (minValue + maxValue) / 2 + } + } + + HStack { + Text("\(Int(minValue)) \(unit)") + Spacer() + Text("\(Int(value)) \(unit)").font(.headline) + Spacer() + Text("\(Int(maxValue)) \(unit)") + } + .font(.caption) + + if !submitted { + Button { + gainedPoints = pointsForEstimation( + guess: value, + correct: correctValue, + minValue: minValue, + maxValue: maxValue + ) + submitted = true + onSubmit(gainedPoints) + } label: { + Text("Antwort abgeben") + .font(.headline) + .padding(.vertical, 10) + .padding(.horizontal, 30) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } else { + let diff = abs(value - correctValue) + let span = maxValue - minValue + let rel = span > 0 ? (diff / span) : 1.0 + + Text("✅ Richtiger Wert: \(Int(correctValue)) \(unit)") + .font(.headline) + .foregroundColor(.green) + Text("Dein Tipp: \(Int(value)) \(unit) — Abweichung: \(Int(diff)) \(unit) (~\(Int(rel*100))%)") + .font(.subheadline) + .foregroundColor(.secondary) + Text("Erhaltene Punkte: \(gainedPoints)") + .font(.headline) + } + } + .padding(.horizontal) + } +} + +private func pointsForEstimation( + guess: Double, + correct: Double, + minValue: Double, + maxValue: Double, + maxPoints: Int = 3, + thresholdPercent: Double = 0.3 +) -> Int { + let span = maxValue - minValue + guard span > 0 else { return 0 } + let absError = abs(guess - correct) + let relError = absError / span + if relError >= thresholdPercent { return 0 } + let ratio = 1.0 - (relError / thresholdPercent) + return max(0, Int(round(Double(maxPoints) * ratio))) +} diff --git a/QuizApp/Views/MultipleChoiceQuestionView.swift b/QuizApp/Views/MultipleChoiceQuestionView.swift new file mode 100644 index 0000000..09f649d --- /dev/null +++ b/QuizApp/Views/MultipleChoiceQuestionView.swift @@ -0,0 +1,66 @@ +// +// MultipleChoiceQuestionView.swift +// QuizApp +// +// Created by Paul on 08.08.25. +// + +import SwiftUI + +struct MultipleChoiceQuestionView: View { + let answers: [String] + let correctIndex: Int + @Binding var selectedIndex: Int? + @Binding var isAnswered: Bool + let onAnswer: (Int) -> Void + let buttonColor: (Int, Int) -> Color + + let onNext: () -> Void + + var body: some View { + VStack { + ForEach(answers.indices, id: \.self) { index in + Button { + selectedIndex = index + isAnswered = true + onAnswer(index) + } label: { + Text(answers[index]) + .padding(.vertical, 12) + .padding(.horizontal, 20) + .frame(maxWidth: 300) + .background(buttonColor(index, correctIndex)) + .foregroundColor(.white) + .cornerRadius(10) + .scaleEffect(isAnswered && selectedIndex == index ? 1.05 : 1.0) + .shadow(radius: isAnswered && selectedIndex == index ? 5 : 2) + } + .animation(.easeInOut(duration: 0.3), value: isAnswered) + .disabled(isAnswered) + } + + VStack { + if isAnswered, let selected = selectedIndex { + let correct = (selected == correctIndex) + Text(correct ? "✅ Richtig!" : "❌ Falsch") + .font(.headline) + .foregroundColor(correct ? .green : .red) + .padding() + } + if isAnswered { + Button(action: onNext) { + Text("Nächste Frage") + .font(.headline) + .padding(.vertical, 10) + .padding(.horizontal, 30) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + } + .frame(height: 100) + } + } +} + diff --git a/QuizApp/Views/QuizFinishedView.swift b/QuizApp/Views/QuizFinishedView.swift new file mode 100644 index 0000000..df2d3d7 --- /dev/null +++ b/QuizApp/Views/QuizFinishedView.swift @@ -0,0 +1,50 @@ +// +// QuizFinishedView.swift +// QuizApp +// +// Created by Paul on 08.08.25. +// + +import SwiftUI + +struct QuizFinishedView: View { + let score: Int + let total: Int + @Binding var playerName: String + let onPlayAgain: () -> Void + let onBackToCategories: () -> Void + let onSaveScore: () -> Void + + var body: some View { + VStack(spacing: 20) { + Text("🎉 Quiz beendet!") + .font(.title) + + Text("Dein Ergebnis: \(score) von \(total) Punkten") + .font(.headline) + + Button("Nochmal spielen", action: onPlayAgain) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + + Button("Zurück zur Kategorieauswahl", action: onBackToCategories) + .padding() + .background(Color.gray) + .foregroundColor(.white) + .cornerRadius(10) + + TextField("Dein Name:", text: $playerName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.horizontal) + + Button("Score speichern", action: onSaveScore) + .padding() + .background(playerName.isEmpty ? Color.gray : Color.green) + .foregroundColor(.white) + .cornerRadius(10) + .disabled(playerName.isEmpty) + } + } +} diff --git a/QuizApp/Views/QuizHeaderView.swift b/QuizApp/Views/QuizHeaderView.swift new file mode 100644 index 0000000..791a34c --- /dev/null +++ b/QuizApp/Views/QuizHeaderView.swift @@ -0,0 +1,36 @@ +// +// QuizHeaderView.swift +// QuizApp +// +// Created by Paul on 08.08.25. +// + +import SwiftUI + +struct QuizHeader: View { + let score: Int + let currentIndex: Int + let total: Int + let colorForStep: (Int) -> Color + + var body: some View { + VStack(spacing: 8) { + Text("Punkte: \(score)") + .font(.headline) + + HStack(spacing: 4) { + ForEach(0..