Home > database >  Non-updating VIews
Non-updating VIews

Time:10-02

I'm fairly new to swift and programming in general but I'm trying to build what is effectively a quiz view that takes quiz questions from a JSON file and displays them onto the screen in a LazyVGrid pattern using a custom "Answer" View.

Here is my custom answer view (sorry about test being all over these names, I have duplicated files to try different things without disturbing my original files!):

struct TestAnswerTest: View {
    @State var answer: String
    @State private var selected = false
    var correctAnswers: [String]

    var body: some View {
        Text(answer)
            .font(.title)
            .bold()
            .frame(width: 165, height: 140)
            .contentShape(RoundedRectangle(cornerRadius: 25))
            .onTapGesture {
                self.selected.toggle()
            }
    }
}

My Question Struct:

struct Question2: Codable, Identifiable {
    let id: Int
    let category: String
    let question: String
    let answers: [String]
    let correctAnswers: [String]
    let options: Int
}

A sample of my json data for reference:

{
    "id": 1,
    "category": "staff",
    "question": "What are the 2 common names for the lines and spaces on sheet music?\n(Select 2 answers)",
    "answers": ["Staff", "Stave", "String", "Stem", "Stake", "Store"],
    "correctAnswers": ["Staff", "Stave"],
    "options": 6
}

And here is my quiz display view:

struct StaffRecap2: View {
    @State private var questionNumber = 0
    private let twoColumnGrid = [GridItem(.flexible()), GridItem(.flexible())]
    let questions:[Question2] = Bundle.main.decode("questions.json")
    
    var body: some View {
        let staffQuestions = questions.filter { $0.category.elementsEqual("staff")}
        
        NavigationView {
            VStack{
                Text(staffQuestions[questionNumber].question)
                
                LazyVGrid(columns: twoColumnGrid) {
                    ForEach(0..<staffQuestions[questionNumber].options, id:\.self) { number in
                        TestAnswerTest(answer: staffQuestions[questionNumber].answers[number],
                                       correctAnswers: staffQuestions[questionNumber].correctAnswers)
                    }
                    .toolbar {
                        ToolbarItemGroup(placement: .bottomBar) {
                            Button {
                                questionNumber  = 1
                                print(staffQuestions[questionNumber])
                            } label: {
                                Text("Next Question")
                            }
                            .disabled(questionNumber == staffQuestions.count)
                        }
                    }
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Gradient(colors: [.teal, .blue]))
            .navigationTitle("Staff Recap")
            .navigationBarTitleDisplayMode(.inline)
            .toolbarBackground(.orange, for: .navigationBar)
            .toolbarBackground(.visible, for: .navigationBar)
        }
    }
}

My issue is that all seems to work fine if I manually change the "questionNumber" property and the code build in simulator and preview with all the correct details but if I use the button to go to the next question it correctly changes the question but doesn't update any of the answer views to the new answer options. I don't really understand why as if I print the correctAnswers array using the onTapGesture modifier in my answer view it also prints the correct array for the question and changes correctly when i press the button.

I'm a bit stumped and I'm sure it is probably something simple I'm missing but i'm at an impasse and would appreciate any help or pointers in the right direction for what I am doing wrong

Thanks in advance!

CodePudding user response:

Here is a very basic example of the approach I mentioned in my comment, that is, using ObservableObject view model, where all the data processing is performed. Note, I've updated the Question model and related json data.

import Foundation
import SwiftUI


class QuestionModel: ObservableObject {
    
    @Published var questions:[Question] = []
    
    init() {
        if let loaded: [Question] = Bundle.main.decode("questions.json") {
            questions = loaded
        }
    }
    
    func addAnswer(to n: Int, answer: Answer) {
        questions[n].answers.append(answer)
    }
    
    func removeAnswer(from n: Int, answer: Answer) {
        questions[n].answers.removeAll(where: {$0.id == answer.id})
    }
    
    func isCorrectAnswer(_ n: Int) -> Bool {
        let arr1 = questions[n].correctAnswers.map{$0.id}
        let arr2 = questions[n].answers.map{$0.id}
        let theSet = Set(arr2).subtracting(arr1)
        return theSet.count == 0 && questions[n].correctAnswers.count == questions[n].answers.count
    }
    
}

struct Question: Codable, Identifiable {
    let id: Int
    var category: String
    let question: String
    var answers: [Answer] // <-- the answers from the user
    var possibles: [Answer] // <-- the possible choices of answers
    var correctAnswers: [Answer] // <-- just the correct answers
}

struct Answer: Codable, Identifiable, Hashable {
    let id: Int
    var answer: String
    var isCorrect: Bool
    var answered: Bool
}

struct ContentView: View {
    @StateObject var model = QuestionModel()
    
    @State private var questionNumber = 0
    @State private var showAnswer = false
    private let twoColumnGrid = [GridItem(.flexible()), GridItem(.flexible())]
    
    var body: some View {
        NavigationView {
            ScrollView {
                Text(model.questions[questionNumber].question)
                LazyVGrid(columns: twoColumnGrid) {
                    ForEach($model.questions[questionNumber].possibles) { $answer in
                        AnswerView(questionNumber: $questionNumber, answer: $answer)
                    }
                }
                NavigationLink("", destination: AnswerInfo(questionNumber: questionNumber), isActive: $showAnswer)
            }
            .toolbar {
                ToolbarItemGroup(placement: .bottomBar) {
                    Button {
                        if questionNumber   1 < model.questions.count {
                            questionNumber  = 1
                        }
                    } label: {
                        Text("Next Question").foregroundColor(.black)
                    }
                    Button {
                        showAnswer = true
                    } label: {
                        Text("Check answer").foregroundColor(.black)
                    }
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Gradient(colors: [.teal, .blue]))
        }
        .environmentObject(model)
        .navigationViewStyle(.stack)
        .navigationTitle("Staff Recap")
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct AnswerInfo: View {
    @EnvironmentObject var model: QuestionModel
    @State var questionNumber: Int
    
    var body: some View {
        VStack {
            if model.isCorrectAnswer(questionNumber) {
                Text("Correct answer").font(.title)
            } else {
                Text("NOT correct answer").font(.title)
            }
            ForEach(model.questions[questionNumber].answers) { answer in
                Text(answer.answer)
            }
        }
    }
}

struct AnswerView: View {
    @EnvironmentObject var model: QuestionModel
    
    @Binding var questionNumber: Int
    @Binding var answer: Answer
    
    var body: some View {
        Button {
            answer.answered.toggle()
            if answer.answered {
                model.addAnswer(to: questionNumber, answer: answer)
            } else {
                model.removeAnswer(from: questionNumber, answer: answer)
            }
        } label: {
            Text(answer.answer)
                .font(.title)
                .bold()
                .frame(width: 165, height: 140)
                .contentShape(RoundedRectangle(cornerRadius: 25))
                .foregroundColor(answer.answered ? Color.red : Color.green)
        }
        .buttonStyle(.bordered)
    }
}

extension Bundle {
    func decode<T: Decodable>(_ file: String) -> T? {
        if let url = self.url(forResource: file, withExtension: nil) {
            do {
                let data = try Data(contentsOf: url)
                return try JSONDecoder().decode(T.self, from: data)
            } catch {
                print(error)
            }
        }
        return nil
    }
}

The json data in the file questions.json

 [
     {
         "id": 1,
         "category": "staff",
         "question": "What are the 2 common names for the lines and spaces on sheet music?\n(Select 2 answers)",
         "answers": [],
         "possibles": [
             {"id": 1, "answer": "Staff", "isCorrect": false, "answered": false},
             {"id": 2, "answer": "Stave", "isCorrect": false, "answered": false},
             {"id": 3, "answer": "String", "isCorrect": false, "answered": false},
             {"id": 4, "answer": "Stem", "isCorrect": false, "answered": false},
             {"id": 5, "answer": "Stake", "isCorrect": false, "answered": false},
             {"id": 6, "answer": "Store", "isCorrect": false, "answered": false},
         ],
         "correctAnswers": [
             {"id": 1, "answer": "Staff", "isCorrect": true, "answered": false},
             {"id": 2, "answer": "Stave", "isCorrect": true, "answered": false}
         ]
     },
     {
         "id": 2,
         "category": "cats",
         "question": "What is this cat?",
         "answers": [],
         "possibles": [
             {"id": 1, "answer": "Cat-1", "isCorrect": false, "answered": false},
             {"id": 2, "answer": "Cat-2", "isCorrect": false, "answered": false},
             {"id": 3, "answer": "Cat-3", "isCorrect": false, "answered": false},
             {"id": 4, "answer": "Cat-4", "isCorrect": false, "answered": false}
         ],
         "correctAnswers": [
             {"id": 2, "answer": "Cat-2", "isCorrect": true, "answered": false}
         ]
     }
 ]
  • Related