Search code examples
swiftswiftui

I can't load the questions. SwiftUI


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
    
    
}
                 








Solution

  • 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.