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}
]
}
]