feat: new file structure
This commit is contained in:
51
QuizApp/Models/QuizQuestion.swift
Normal file
51
QuizApp/Models/QuizQuestion.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,17 +25,17 @@ class ViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentQuestion: QuizQuestion? {
|
var currentQuestion: QuizQuestion? {
|
||||||
guard currentQuestionIndex < questions.count else { return nil }
|
guard currentQuestionIndex < questions.count else { return nil }
|
||||||
return questions[currentQuestionIndex]
|
return questions[currentQuestionIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableCategories: [String] {
|
var availableCategories: [String] {
|
||||||
return Array(allQuestionsByCategory.keys).sorted()
|
return Array(allQuestionsByCategory.keys).sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else {
|
guard let url = Bundle.main.url(forResource: "questions", withExtension: "json") else {
|
||||||
questions = []
|
questions = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,7 @@ class ViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
allQuestionsByCategory = try JSONDecoder().decode([String: [QuizQuestion]].self, from: data)
|
allQuestionsByCategory = try JSONDecoder().decode([String: [QuizQuestion]].self, from: data)
|
||||||
|
print("Fragen geladen")
|
||||||
} catch {
|
} catch {
|
||||||
print("Fehler beim Laden des Inhalts: \(error)")
|
print("Fehler beim Laden des Inhalts: \(error)")
|
||||||
allQuestionsByCategory = [:]
|
allQuestionsByCategory = [:]
|
||||||
@@ -66,7 +67,7 @@ class ViewModel: ObservableObject {
|
|||||||
answeredCount = 0
|
answeredCount = 0
|
||||||
selectedAnswers = Array(repeating: nil, count: questions.count)
|
selectedAnswers = Array(repeating: nil, count: questions.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func incrementScore(selectedIndex: Int) {
|
func incrementScore(selectedIndex: Int) {
|
||||||
if let correct = currentQuestion?.correctAnswer, selectedIndex == correct {
|
if let correct = currentQuestion?.correctAnswer, selectedIndex == correct {
|
||||||
@@ -20,115 +20,70 @@ struct ContentView: View {
|
|||||||
@State private var playerName: String = ""
|
@State private var playerName: String = ""
|
||||||
@State private var scoreIsSaved = false
|
@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 {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if viewModel.isQuizFinished {
|
if viewModel.isQuizFinished {
|
||||||
VStack(spacing: 20) {
|
QuizFinishedView(
|
||||||
Text("🎉 Quiz beendet!")
|
score: viewModel.score,
|
||||||
.font(.title)
|
total: viewModel.questions.count,
|
||||||
|
playerName: $playerName,
|
||||||
Text("Dein Ergebnis: \(viewModel.score) von \(viewModel.questions.count) Punkten")
|
onPlayAgain: { restartQuiz() },
|
||||||
.font(.headline)
|
onBackToCategories: { dismiss() },
|
||||||
|
onSaveScore: {
|
||||||
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") {
|
|
||||||
saveScore()
|
saveScore()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.padding()
|
)
|
||||||
.background(playerName.isEmpty ? Color.gray : Color.green)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.disabled(playerName.isEmpty)
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
AudioServicesPlaySystemSound(1322)
|
AudioServicesPlaySystemSound(1322)
|
||||||
score += viewModel.score
|
score += viewModel.score
|
||||||
}
|
}
|
||||||
} else if let frage = viewModel.currentQuestion {
|
} else if let frage = viewModel.currentQuestion {
|
||||||
Text("Punkte: \(viewModel.score)")
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
|
|
||||||
//Fortschrittsbalken
|
QuizHeader(
|
||||||
VStack(spacing: 4) {
|
score: viewModel.score,
|
||||||
HStack(spacing: 4) {
|
currentIndex: viewModel.currentQuestionIndex,
|
||||||
ForEach(0..<viewModel.questions.count, id: \.self) { index in
|
total: viewModel.questions.count,
|
||||||
Rectangle()
|
colorForStep: { idx in colorForAnswer(at: idx) } // nutzt deine bestehende Funktion
|
||||||
.fill(colorForAnswer(at: index))
|
)
|
||||||
.frame(width: 20, height: index == viewModel.currentQuestionIndex ? 14 : 10)
|
|
||||||
.cornerRadius(3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("Frage \(viewModel.currentQuestionIndex + 1) von \(viewModel.questions.count)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
|
|
||||||
Text(frage.question)
|
Text(frage.question)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
ForEach(frage.answers.indices, id: \.self) { index in
|
if frage.isEstimation,
|
||||||
Button(action: {
|
let minV = frage.minValue,
|
||||||
selectedAnswerIndex = index
|
let maxV = frage.maxValue,
|
||||||
isAnswered = true
|
let correctV = frage.correctValue {
|
||||||
print("Selected:", index)
|
// Estimation
|
||||||
print("Correct:", frage.correctAnswer)
|
EstimationQuestionView(
|
||||||
viewModel.incrementScore(selectedIndex: index)
|
minValue: minV,
|
||||||
|
maxValue: maxV,
|
||||||
|
correctValue: correctV,
|
||||||
|
unit: frage.unit ?? "",
|
||||||
|
value: $estimationValue,
|
||||||
|
submitted: $estimationSubmitted,
|
||||||
|
gainedPoints: $estimationPoints
|
||||||
|
) { gained in
|
||||||
|
viewModel.score += gained
|
||||||
viewModel.answeredCount += 1
|
viewModel.answeredCount += 1
|
||||||
viewModel.selectedAnswers[viewModel.currentQuestionIndex] = index
|
isAnswered = true
|
||||||
}) {
|
|
||||||
Text(frage.answers[index])
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.frame(maxWidth: 300)
|
|
||||||
.background(buttonColor(for: index, correctIndex: frage.correctAnswer))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.scaleEffect(isAnswered && selectedAnswerIndex == index ? 1.05 : 1.0)
|
|
||||||
.shadow(radius: isAnswered && selectedAnswerIndex == index ? 5 : 2)
|
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.3), value: isAnswered)
|
|
||||||
.disabled(isAnswered)
|
|
||||||
|
|
||||||
}
|
if estimationSubmitted {
|
||||||
VStack {
|
Button {
|
||||||
if isAnswered {
|
|
||||||
Text(selectedAnswerIndex == frage.correctAnswer ? "✅ Richtig!" : "❌ Falsch")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(selectedAnswerIndex == frage.correctAnswer ? .green : .red)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
if isAnswered {
|
|
||||||
Button(action: {
|
|
||||||
viewModel.loadNextQuestion()
|
viewModel.loadNextQuestion()
|
||||||
selectedAnswerIndex = nil
|
// Reset für nächste Estimation
|
||||||
|
estimationSubmitted = false
|
||||||
isAnswered = false
|
isAnswered = false
|
||||||
}) {
|
selectedAnswerIndex = nil
|
||||||
|
estimationPoints = 0
|
||||||
|
} label: {
|
||||||
Text("Nächste Frage")
|
Text("Nächste Frage")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@@ -137,9 +92,33 @@ struct ContentView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Multiple-Choice
|
||||||
|
MultipleChoiceQuestionView(
|
||||||
|
answers: frage.answers,
|
||||||
|
correctIndex: frage.correctAnswer,
|
||||||
|
selectedIndex: $selectedAnswerIndex,
|
||||||
|
isAnswered: $isAnswered,
|
||||||
|
onAnswer: { index in
|
||||||
|
print("Selected:", index)
|
||||||
|
print("Correct:", frage.correctAnswer)
|
||||||
|
viewModel.incrementScore(selectedIndex: index)
|
||||||
|
viewModel.answeredCount += 1
|
||||||
|
viewModel.selectedAnswers[viewModel.currentQuestionIndex] = index
|
||||||
|
},
|
||||||
|
buttonColor: { idx, correct in
|
||||||
|
buttonColor(for: idx, correctIndex: correct)
|
||||||
|
},
|
||||||
|
onNext: {
|
||||||
|
viewModel.loadNextQuestion()
|
||||||
|
selectedAnswerIndex = nil
|
||||||
|
isAnswered = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.frame(height: 100)
|
|
||||||
} else {
|
} else {
|
||||||
Text("Fragen werden geladen...")
|
Text("Fragen werden geladen...")
|
||||||
}
|
}
|
||||||
@@ -234,6 +213,17 @@ struct ContentView: View {
|
|||||||
|
|
||||||
print("Score gespeichert: \(playerName) - \(viewModel.score) Punkte")
|
print("Score gespeichert: \(playerName) - \(viewModel.score) Punkte")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
99
QuizApp/Views/EstimationQuestionView.swift
Normal file
99
QuizApp/Views/EstimationQuestionView.swift
Normal file
@@ -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)))
|
||||||
|
}
|
||||||
66
QuizApp/Views/MultipleChoiceQuestionView.swift
Normal file
66
QuizApp/Views/MultipleChoiceQuestionView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
50
QuizApp/Views/QuizFinishedView.swift
Normal file
50
QuizApp/Views/QuizFinishedView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
QuizApp/Views/QuizHeaderView.swift
Normal file
36
QuizApp/Views/QuizHeaderView.swift
Normal file
@@ -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..<total, id: \.self) { idx in
|
||||||
|
Rectangle()
|
||||||
|
.fill(colorForStep(idx))
|
||||||
|
.frame(width: 20, height: idx == currentIndex ? 14 : 10)
|
||||||
|
.cornerRadius(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Frage \(currentIndex + 1) von \(total)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user