Home > Net >  array seems to be empty and can not be used with ForEach loop
array seems to be empty and can not be used with ForEach loop

Time:04-21

I am working on this small app. I've defined the AnswerRow view as below

struct AnswerRow: View {
    @EnvironmentObject var questionManager: QuestionManager
    @State private var isSelected = false
    var answer: Answer
    var green = Color(hue: 0.437, saturation: 0.711, brightness: 0.711)
    var red = Color(red: 0.71, green: 0.094, blue: 0.1)
    
    var body: some View {
        HStack(spacing: 20) {
            Image(systemName: "circle.fill")
                .foregroundColor(.accentColor)
                .font(.caption)
            
            Text(answer.text)
                .foregroundColor(.accentColor)
                .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(questionManager.answerSelected ? (isSelected ? Color("AccentColor") : .gray) : Color("AccentColor"))
        .background(.white)
        .cornerRadius(10)
        .shadow(color: isSelected ? answer.isCorrect ? green : red : .gray, radius: 5, x: 0.5, y: 0.5)
        .onTapGesture {
            if !questionManager.answerSelected {
                isSelected = true
                questionManager.selectAnswer(answer: answer)

            }
        }
    }
}

and here in my QuestionView I've tried to call the AnswerRow with ForEach, optional codes are there to check if the array is working which shows that the array has 0 members, which I don't understand

>>QuestionView_swift

struct QuestionView: View {
    @EnvironmentObject var questionManager : QuestionManager
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            
            Text("number of choices \(questionManager.answerChoices.count)") 
>>to check if the array is defined
            AnswerRow(answer: Answer(id: 1, text: "test Choice", isCorrect: true)) 
>> to check if the View is working without 'ForEach'
            
            ForEach(questionManager.answerChoices, id: \.id) { answer in
                AnswerRow(answer: answer)
                    .environmentObject(questionManager)
            }
        }
    }
}

My Models are as follows, Question model:

>>Question_swift

struct Question: Identifiable, Codable {
    var id: Int
    var question: String
    var correctAnswer : String
    var incorrectAnswers : [String]
    var type : String
    var imageName: String
    
    var answers: [Answer] {
        
            
            let correct = [Answer(id: id, text: correctAnswer, isCorrect: true)]
            let incorrects = incorrectAnswers.map { answer in
                Answer(id: id, text: answer, isCorrect: false)
            }
            let allAnswers = correct   incorrects
            
            return allAnswers.shuffled()
        
    }
}


    
    var questions: [Question] = load("questions.json")

    func load<T: Decodable>(_ filename: String) -> T {
        let data: Data

        guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
            else {
                fatalError("Couldn't find \(filename) in main bundle.")
        }

        do {
            data = try Data(contentsOf: file)
        } catch {
            fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
        }

        do {
            let decoder = JSONDecoder()
            return try decoder.decode(T.self, from: data)
        } catch {
            fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
        }
    }

and Answer model as

>> Answer_swift

struct Answer: Identifiable {
    var id : Int
    var text: String
    var isCorrect: Bool
}

I think there are some problem with my QuestionManager, may be it is responsible for the array to fail.

>>QuestionManager_swift

import Foundation
import SwiftUI
class QuestionManager : ObservableObject {
    @Published private(set) var questions : [Question] = []
    @Published private(set) var answerSelected = false
    @Published private(set) var reachedEnd = false
    @Published private(set) var score = 0
    @Published private(set) var progress: CGFloat = 0.00
    @Published private(set) var index = 0
    @Published private(set) var answerChoices: [Answer] = []
    @Published private(set) var question = ""
    @Published var markedQuestions : [Int] = []

    
    
    
    
    
    func selectAnswer(answer: Answer) {
        answerSelected = true
        
        if answer.isCorrect {
            score  = 1
        } else {
            markedQuestions.append(index)
        }
    }
    
    func goToNextQuestion() {
        if index   1 < questions.count {
            index  = 1
            setQuestion()
        } else {
            reachedEnd = true
        }
    }
    
    func setQuestion() {
        answerSelected = false
        progress = CGFloat(Double((index   1)) / Double(questions.count) * 350)

        
        if index < questions.count {
            let currentQuestion = questions[index]
            question = currentQuestion.question
            answerChoices = currentQuestion.answers
        }
    }
    
    
}

I'm completely new so please help me in layman's term. Thank you all.

CodePudding user response:

You have 2 different [Question] arrays.

One in your Viewmodel:

@Published private(set) var questions : [Question] = []

and one that seems to be a global var:

var questions: [Question] = load("questions.json")

only the global var is populated with the data from your load function.

Remove the global one, move the load function to your QuestionManager and add this initializer to it:

init(){
    questions = load("questions.json")
}

But you would still need to populate your question so change your QuestionView to:

struct QuestionView: View {
    @EnvironmentObject var questionManager : QuestionManager
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            
            Text("number of choices \(questionManager.answerChoices.count)")
            AnswerRow(answer: Answer(id: 1, text: "test Choice", isCorrect: true))
            
            ForEach(questionManager.answerChoices, id: \.id) { answer in
                AnswerRow(answer: answer)
                    .environmentObject(questionManager)
            }
        }.onAppear{
            questionManager.setQuestion() //Add this
        }
    }
}

Edit to adress the comment:

You are not seeing all answers because you are assigning the same id to them. And in your ForEach you are setting this id explicitly to be the id of the View.

Did you see this error in the debug area?

ForEach<Array, Int, ModifiedContent<AnswerRow, _EnvironmentKeyWritingModifier<Optional>>>: the ID 1 occurs multiple times within the collection, this will give undefined results!

Change your model to:

struct Answer: Identifiable {
    var id = UUID()
    var text: String
    var isCorrect: Bool
}

struct Question: Identifiable, Codable {
    var id: Int
    var question: String
    var correctAnswer : String
    var incorrectAnswers : [String]
    var type : String
    var imageName: String
    
    var answers: [Answer] {
        let correct = [Answer(text: correctAnswer, isCorrect: true)]
        let incorrects = incorrectAnswers.map { answer in
            Answer(text: answer, isCorrect: false)
        }
        let allAnswers = correct   incorrects
        
        return allAnswers.shuffled()
    }
}

The answer struct isn´t persisted anyway, so you can create a new id every time.

  • Related