英文:
I can't load the questions. SwiftUI
问题
Here's the translated part of your code:
import SwiftUI
struct ContentView: View {
@StateObject var gameManager = GameManagerVM()
var body: some View {
NavigationView {
ZStack {
LinearGradient(gradient: .init(colors: [.mint,.indigo]), startPoint: .zero, endPoint: .center)
.ignoresSafeArea()
VStack(spacing:250) {
Text("QUIZ GAME")
.font(.system(size: 50))
.bold()
.foregroundColor(.white)
.shadow(radius: 5)
VStack {
VStack(spacing: 20) {
NavigationLink {
HistoryQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text: "History")
}
NavigationLink {
GeographyQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text: "Geography")
}
NavigationLink {
SportQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text: "Sport")
}
}
}
.frame(width: 350, height: 400)
.background(.ultraThinMaterial)
.cornerRadius(40)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewInterfaceOrientation(.portrait)
}
}
Please let me know if you need further assistance or if you'd like me to translate another part of your code.
英文:
Good day. I am trying to create a quiz game in SwiftUI following MVVM. The game has three categories of questions (History, Geography and Sports). There are 50 questions in each category.
However, I have a problem with the fact that I cannot upload questions from the geography category to GeographyQuizView and also questions from the sport category to SportQuizView. I was able to load only questions from the history category into HistoryQuizView. What is the problem and how to fix it. Thank you.
import SwiftUI
struct ContentView: View {
@StateObject var gameManager = GameManagerVM()
var body: some View {
NavigationView {
ZStack {
LinearGradient(gradient: .init(colors: [.mint,.indigo]), startPoint: .zero, endPoint: .center)
.ignoresSafeArea()
VStack(spacing:250) {
Text("QUIZ GAME")
.font(.system(size: 50))
.bold()
.foregroundColor(.white)
.shadow(radius: 5)
VStack {
VStack(spacing: 20) {
NavigationLink {
HistoryQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text:"History")
}
NavigationLink {
GeographyQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text: "Geography")
}
NavigationLink {
SportQuizView()
.navigationBarHidden(true)
.environmentObject(gameManager)
}label: {
CategoryButton(text:"Sport")
}
}
}
.frame(width: 350, height: 400)
.background(.ultraThinMaterial)
.cornerRadius(40)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewInterfaceOrientation(.portrait)
}
}
import SwiftUI
struct GeographyQuizView: View {
@EnvironmentObject var gameManager: GameManagerVM
var body: some View {
VStack(spacing: 50){
VStack {
Text("\(gameManager.index + 1) out of \(gameManager.lenght)")
.foregroundColor(.white)
.fontWeight(.heavy)
ProgressBar(progress: gameManager.progress)
}
VStack(alignment: .leading, spacing: 20) {
if gameManager.category == "Geography"{
Text(gameManager.question)
.font(.system(size:20))
.bold()
.foregroundColor(.black)
ForEach(gameManager.answerChoices, id: \.id) { answer in AnswerDisplay(answer: answer)
.environmentObject(gameManager)
}
}
}
Button {
gameManager.nextQuestion()
} label: {
NextButton(text: "Next")
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(LinearGradient(gradient: .init(colors: [.indigo, .white]), startPoint: .zero, endPoint:.bottom))
}
}
struct GeographyQuizView_Previews: PreviewProvider {
static var previews: some View {
GeographyQuizView()
.environmentObject(GameManagerVM())
}
}
import SwiftUI
struct HistoryQuizView: View {
@EnvironmentObject var gameManager: GameManagerVM
var body: some View {
VStack(spacing: 50){
VStack {
Text("\(gameManager.index + 1) out of \(gameManager.lenght)").foregroundColor(.white)
.fontWeight(.heavy)
ProgressBar(progress: gameManager.progress)
}
VStack(alignment: .leading, spacing: 20) {
if gameManager.category == "History"{
Text(gameManager.question)
.font(.system(size:20))
.bold()
.foregroundColor(.black)
ForEach(gameManager.answerChoices, id: \.id) { answer in AnswerDisplay(answer: answer)
.environmentObject(gameManager)
}
}
}
Button {
gameManager.nextQuestion()
} label: {
NextButton(text: "Next")
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(LinearGradient(gradient: .init(colors: [.indigo, .white]), startPoint: .zero, endPoint:.bottom))
}
}
struct HistoryQuizView_Previews: PreviewProvider {
static var previews: some View {
HistoryQuizView()
.environmentObject(GameManagerVM())
}
}
import SwiftUI
struct SportQuizView: View {
@EnvironmentObject var gameManager: GameManagerVM
var body: some View {
VStack(spacing: 50) {
VStack {
Text("\(gameManager.index + 1) out of \(gameManager.lenght)").foregroundColor(.white)
.fontWeight(.heavy)
ProgressBar(progress: gameManager.progress)
}
VStack(alignment: .leading, spacing: 20) {
if gameManager.category == "Sports"{
Text(gameManager.question)
.font(.system(size: 20))
.bold()
.foregroundColor(.black)
ForEach(gameManager.answerChoices, id: \.id) { answer in
AnswerDisplay(answer: answer)
.environmentObject(gameManager)
}
}
}
Button {
gameManager.nextQuestion()
} label: {
NextButton(text: "Next")
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(LinearGradient(gradient: .init(colors: [.indigo, .white]), startPoint: .zero, endPoint: .bottom))
}
}
struct SportQuizView_Previews: PreviewProvider {
static var previews: some View {
SportQuizView()
.environmentObject(GameManagerVM())
}
}
import Foundation
import SwiftUI
class GameManagerVM: ObservableObject {
private (set)var quiz: [Quiz.Result] = []
@Published private(set)var lenght = 0
@Published private(set)var index = 0
@Published private(set)var reachedEnd = false
@Published private(set)var answerIsSelected = false
@Published private(set)var question: AttributedString = ""
@Published private(set)var answerChoices: [Answer] = []
@Published private(set)var progress: CGFloat = 0.00
@Published private(set)var score = 0
@Published private(set)var category = ""
init() {
Task.init {
await fetchQuiz()
}
}
func fetchQuiz() async {
let categoryURLs = [
"https://opentdb.com/api.php?amount=50&category=21&difficulty=medium&type=multiple",
"https://opentdb.com/api.php?amount=50&category=22&difficulty=medium&type=multiple",
"https://opentdb.com/api.php?amount=50&category=23&difficulty=medium&type=multiple"
]
for urlStr in categoryURLs {
guard let url = URL(string: urlStr) else {
fatalError("Invalid URL: \(urlStr)")
}
do {
let urlRequest = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
fatalError("Error while fetching data")
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decodedData = try decoder.decode(Quiz.self, from: data)
DispatchQueue.main.async {
self.quiz = decodedData.results
self.lenght = self.quiz.count
if self.quiz.count >= 50 {
self.setQuestion()
}
}
} catch {
print("Error fetching trivia: \(error)")
}
}
}
func nextQuestion() {
if index + 1 < lenght {
index += 1
setQuestion()
} else {
reachedEnd = true
}
}
func setQuestion() {
answerIsSelected = false
progress = CGFloat(Double(index + 1) / Double(lenght) * 350)
if index < lenght {
let currentTriviaQuestion = quiz[index]
question = currentTriviaQuestion.formattedQuestion
answerChoices = currentTriviaQuestion.answers
category = currentTriviaQuestion.category
}
}
func selectAnswer(answer: Answer) {
answerIsSelected = true
if answer.isCorrect {
score += 1
}
}
}
import SwiftUI
struct CategoryButton: View {
var text: String
var background: Color = Color.indigo
var body: some View {
Text(text)
.frame(width: 200, height: 80)
.foregroundColor(.white)
.background(background)
.cornerRadius(50)
.shadow(radius: 20)
.font(.largeTitle.bold())
.padding()
}
}
struct CategoryButton_Previews: PreviewProvider {
static var previews: some View {
CategoryButton(text: "Category")
}
}
import SwiftUI
struct AnswerDisplay: View {
@EnvironmentObject var gameManager: GameManagerVM
var answer: Answer
@State private var isSelected = false
var green = Color.green
var red = Color.red
var body: some View {
HStack(spacing: 20) {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text(answer.text)
.bold()
if isSelected {
Spacer()
Image(systemName: answer.isCorrect ? "checkmark.circle.fill" : "x.circle.fill")
.foregroundColor(answer.isCorrect ? green:red)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(gameManager.answerIsSelected ? (isSelected ? Color(.black):.yellow): Color(.black))
.background(.white)
.cornerRadius(10)
.shadow(color: .indigo, radius: 5, x: 0.5, y:0.5)
.onTapGesture {
if !gameManager.answerIsSelected {
isSelected = true
gameManager.selectAnswer(answer: answer)
}
}
}
}
struct AnswerDisplay_Previews: PreviewProvider {
static var previews: some View {
AnswerDisplay(answer: Answer(text: "Ljuba", isCorrect: false))
.environmentObject(GameManagerVM())
}
}
import SwiftUI
struct ProgressBar: View {
var progress: CGFloat
var body: some View {
ZStack(alignment: .leading) {
Rectangle()
.frame(maxWidth: 350, maxHeight: 4)
.foregroundColor(Color.gray)
.cornerRadius(10)
Rectangle()
.frame(width: min(progress, 350), height: 4)
.foregroundColor(Color.red)
.cornerRadius(10)
}
}
}
struct ProgressBar_Previews: PreviewProvider {
static var previews: some View {
ProgressBar(progress: 50)
}
}
import SwiftUI
struct NextButton: View {
var text: String
var body: some View {
let background: Color = Color.yellow
Text(text)
.frame(width: 90, height: 65)
.foregroundColor(.white)
.background(background)
.cornerRadius(20)
.font(.largeTitle.bold())
.shadow(radius: 5)
.padding()
}
}
struct NextButton_Previews: PreviewProvider {
static var previews: some View {
NextButton(text: "Next")
}
}
import Foundation
import SwiftUI
struct Quiz: Decodable {
var results: [Result]
struct Result: Decodable, Identifiable {
var id: UUID {
UUID()
}
var category: String
var type:String
var difficulty:String
var question: String
var correctAnswer: String
var incorrectAnswers: [String]
var formattedQuestion: AttributedString {
do {
return try AttributedString(markdown: question)
} catch {
print("Error setting formattedQuetion: \(error)")
return ""
}
}
var answers: [Answer] {
do{
let correct = [Answer(text:try AttributedString(markdown: correctAnswer), isCorrect: true)]
let incorrects = try incorrectAnswers.map { answer in Answer(text: try AttributedString(markdown: answer), isCorrect: false)
}
let allAnswers = correct + incorrects
return allAnswers.shuffled()
} catch {
print("Error setting answer")
return[]
}
}
}
}
import Foundation
struct Answer: Identifiable {
var id = UUID()
var text: AttributedString
var isCorrect: Bool
}
答案1
得分: 0
欢迎来到 Stack Overflow!您的 GameManagerVM.fetchQuiz()
存在一个缺陷。您正在遍历您的 URLSession
调用,每种类型一个,体育、地理和历史,按顺序进行。每个调用都会发送所请求的数据。然而,您尝试将它们存储在同一个变量 quiz
中。因此,每次获得新数据时,旧数据都会被替换。这发生在您的代码中的这里:
DispatchQueue.main.async {
self.quiz = decodedData.results // This overwrites the last saved data
self.lenght = self.quiz.count
if self.quiz.count >= 50 {
self.setQuestion()
}
}
我建议将 quiz
存储为一个 Quiz.Result
字典,而不是数组。它会看起来像 [String:[Quiz.Result]]
,其中 String
是一个 category
。然后,每次下载数据时,您可以将其存储在其自己的类别中。
这会改变上面的部分:
DispatchQueue.main.async {
// 返回的数据都是同一类别的,所以您可以从第一个元素中提取数据
// 如果失败,你就没有第一个元素,所以你也不想要数据
if let category = decodedData.results.first?.category {
self.quiz[category] = decodedData.results
self.lenght = self.quiz.count
...
}
}
您可以使用类似这样的简单 UI 来检查是否获得了所有类别和问题:
struct ContentView: View {
@StateObject var gameManager = GameManagerVM()
var body: some View {
NavigationView {
List {
// 字典是无序的,所以不能直接在 ForEach 中使用
// 这里,我已将键转换为一个字符串数组
// 使用 self 作为 id(在这里足够安全,因为类别必须是唯一的)
ForEach(Array(gameManager.quiz.keys), id:\.self) { category in
Section(category) {
// 因为您使用的是字典,通过键访问将返回一个可选值
// 但是您知道数据对于键是存在的,否则您就不会有这个键
// 因此,去除可选值的最简单方法是与空数组一起使用 nil 合并
ForEach(gameManager.quiz[category] ?? []) { quizQuestion in
Text(quizQuestion.question)
}
}
}
}
}
}
}
我会让您自行更新代码以处理 Dict
而不是 Array
。这并不太困难,而且有许多资源可供使用。如果遇到困难,您可以在 Stack Overflow 上提一个新问题。
最后,请尽量清晰地发布问题和显示问题的最小代码。
英文:
Welcome to Stack Overflow! Your GameManagerVM.fetchQuiz()
has a flaw in it. You are iterating over your URLSession
calls, one for each type, Sports, Geography and History, in that order. Each one of the calls is sending back the requested data. However, you are attempting to store them in the same variable, quiz
. So, each time you get new data, the old data is being replaced. This happens here in your code:
DispatchQueue.main.async {
self.quiz = decodedData.results // This overwrites the last saved data
self.lenght = self.quiz.count
if self.quiz.count >= 50 {
self.setQuestion()
}
}
I would consider storing quiz
not as an array of Quiz.Result
, but rather as a dict. It would look like this [String:[Quiz.Result]]
, where String
is a category
. Then, each time you download data, you can store it in its own category.
That would change the above section to:
DispatchQueue.main.async {
// The returned data is all one category, so you can extract the data
// from the first element. If this fails, you don't have a first element
// so you got no data, so you don't want it anyway
if let category = decodedData.results.first?.category {
self.quiz[category] = decodedData.results
self.lenght = self.quiz.count
...
}
}
You can check that you got all your categories and questions with a simple UI like this:
struct ContentView: View {
@StateObject var gameManager = GameManagerVM()
var body: some View {
NavigationView {
List {
// Dictionaries are unordered, so they can't be used as is in a
// ForEach. Here, I have turned the keys into an array of
// String, with an id of self (safe enough here as the
// categories must be unique.
ForEach(Array(gameManager.quiz.keys), id:\.self) { category in
Section(category) {
// because you are using a dictionary, accessing
// by the key will return an optional However,
// you know the data exists for the key, else
// you wouldn't have the key, so the easiest
// way to remove the optional is to nil coalesce
// with an empty array
ForEach(gameManager.quiz[category] ?? []) { quizQuestion in
Text(quizQuestion.question)
}
}
}
}
}
}
}
I will let you work through updating the code to handle a Dict
instead of an Array
. It is not too difficult, and there are many resources. If you get stuck, you can ask a new question on Stack Overflow.
The last point is to please try and post a clear question and the minimal code to show the question.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论