Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions CloudMaster/Features/Exam/Views/ExamView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,27 @@ struct ExamView: View {
}
.padding(.horizontal)

let question = questions[currentQuestionIndex]

QuestionView(
mode: .exam,
question: questions[currentQuestionIndex],
selectedChoices: selectedChoices[questions[currentQuestionIndex].id] ?? [],
isMultipleResponse: questions[currentQuestionIndex].multipleResponse,
question: question,
selectedChoices: selectedChoices[question.id] ?? [],
isMultipleResponse: question.multipleResponse,
isResultShown: false, // Exam mode does not show result immediately
onChoiceSelected: { choiceId in
if questions[currentQuestionIndex].multipleResponse {
if selectedChoices[questions[currentQuestionIndex].id]?.contains(choiceId) == true {
selectedChoices[questions[currentQuestionIndex].id]?.remove(choiceId)
if question.multipleResponse {
if selectedChoices[question.id]?.contains(choiceId) == true {
selectedChoices[question.id]?.remove(choiceId)
} else {
selectedChoices[questions[currentQuestionIndex].id, default: []].insert(choiceId)
selectedChoices[question.id, default: []].insert(choiceId)
}
} else {
selectedChoices[questions[currentQuestionIndex].id] = [choiceId]
selectedChoices[question.id] = [choiceId]
}
}
)
.id(question.id)

Button(action: {
if currentQuestionIndex < questions.count - 1 {
Expand Down
11 changes: 11 additions & 0 deletions CloudMaster/Features/Settings/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ struct SettingsView: View {
Text("Delete all previous exams")
}
}

Section(header: Text("Preferences")) {
Toggle("Shuffle question choices", isOn: Binding(
get: {
UserDefaults.standard.bool(forKey: "shuffleQuestionChoices")
},
set: { shuffleQuestionChoices in
UserDefaults.standard.set(shuffleQuestionChoices, forKey: "shuffleQuestionChoices")
}
))
}
}
.navigationBarTitle("Settings", displayMode: .inline)
.confirmPopup(isPresented: $showAlert, title: alertTitle, message: alertMessage, confirmAction: {
Expand Down
4 changes: 2 additions & 2 deletions CloudMaster/Features/Shared/Components/QuestionImages.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

struct QuestionImages: View {
let images: [Question.ImageInfo]
let images: [ImageInfo]
@Binding var currentImageIndex: Int
@Binding var isFullscreenImageShown: Bool
@Binding var selectedImageIndex: Int
Expand Down Expand Up @@ -53,7 +53,7 @@ struct QuestionImages: View {


struct FullscreenImageView: View {
let images: [Question.ImageInfo]
let images: [ImageInfo]
@Binding var selectedImageIndex: Int
@Binding var isShown: Bool

Expand Down
59 changes: 53 additions & 6 deletions CloudMaster/Features/Shared/Components/QuestionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ struct QuestionView: View {
@State private var currentImageIndex = 0
@State private var isFullscreenImageShown = false
@State private var selectedImageIndex = 0
@State private var shuffledQuestion: Question?

@State private var shuffleQuestionChoices: Bool = UserDefaults.standard.bool(forKey: "shuffleQuestionChoices")

var body: some View {
let displayQuestion = shuffledQuestion ?? question

ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(question.question)
.font(.system(size: adjustedFontSize(for: question.question), weight: .bold))
Text(displayQuestion.question)
.font(.system(size: adjustedFontSize(for: displayQuestion.question), weight: .bold))
.minimumScaleFactor(0.5)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal)
.multilineTextAlignment(.leading)
.lineSpacing(2)

QuestionImages(images: question.images,
QuestionImages(images: displayQuestion.images,
currentImageIndex: $currentImageIndex,
isFullscreenImageShown: $isFullscreenImageShown,
selectedImageIndex: $selectedImageIndex)
Expand All @@ -41,7 +46,7 @@ struct QuestionView: View {

if isMultipleResponse {
VStack {
Text("Multiple response - Pick \(question.responseCount)")
Text("Multiple response - Pick \(displayQuestion.responseCount)")
.font(.subheadline)
.multilineTextAlignment(.center)
.opacity(0.7)
Expand All @@ -53,7 +58,7 @@ struct QuestionView: View {
.padding(.horizontal)
}

ForEach(question.choices) { choice in
ForEach(displayQuestion.choices) { choice in
if mode == .training {
TrainingChoice(
choice: choice,
Expand All @@ -75,17 +80,59 @@ struct QuestionView: View {
}
}
}
.onAppear {
shuffleCurrentQuestionChoices()
}
.onChange(of: question.id) { _ in
shuffleCurrentQuestionChoices()
}
.padding()
}
.overlay(
Group {
if isFullscreenImageShown {
FullscreenImageView(images: question.images, selectedImageIndex: $selectedImageIndex, isShown: $isFullscreenImageShown)
FullscreenImageView(images: displayQuestion.images, selectedImageIndex: $selectedImageIndex, isShown: $isFullscreenImageShown)
}
}
)
}

private func shuffleCurrentQuestionChoices() -> Void {
if (!shuffleQuestionChoices) {
shuffledQuestion = question;
return;
}

let choices = getShuffledChoices(choices: question.choices, images: question.images, mode: mode)
shuffledQuestion = Question(
id: question.id,
question: question.question,
choices: choices,
multipleResponse: question.multipleResponse,
responseCount: question.responseCount,
images: question.images
)
}

private func getShuffledChoices(choices: [Choice], images: [ImageInfo], mode: Mode) -> [Choice] {
if (mode == .bookmarked) {
return choices; // Do not shuffle bookmarked choices
}

// Do not shuffle choices which belongs to an image (an image is available for each choice), because reference in the answer is lost
if (images.count > 1) {
return choices;
}

var shuffledChoices = choices;
shuffledChoices.shuffle();
return shuffledChoices;
}

private func _debug(question: Question) {
print(question)
}

private func adjustedFontSize(for text: String) -> CGFloat {
let baseFontSize: CGFloat = 20
let minFontSize: CGFloat = 14
Expand Down
1 change: 1 addition & 0 deletions CloudMaster/Features/Training/Views/TrainingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct TrainingView: View {
handleChoiceSelection(choiceID, question)
}
)
.id(question.id)
.navigationBarItems(trailing: bookmarkButton)
.onAppear {
updateBookmarkState()
Expand Down
24 changes: 16 additions & 8 deletions CloudMaster/Utilities/QuestionLoader.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import Foundation

struct Question: Identifiable, Codable {
let id = UUID()
var id = UUID()
let question: String
let choices: [Choice]
var choices: [Choice]
var multipleResponse: Bool
var responseCount: Int
let images: [ImageInfo]
var images: [ImageInfo]

enum CodingKeys: String, CodingKey {
case question, choices, multipleResponse = "multiple_response", responseCount = "response_count", images
}

struct ImageInfo: Codable {
let path: String
let url: String?
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
question = try container.decode(String.self, forKey: .question)
Expand All @@ -25,8 +20,21 @@ struct Question: Identifiable, Codable {
responseCount = try container.decodeIfPresent(Int.self, forKey: .responseCount) ?? 0
images = try container.decodeIfPresent([ImageInfo].self, forKey: .images) ?? []
}

init(id: UUID, question: String, choices: [Choice], multipleResponse: Bool, responseCount: Int, images: [ImageInfo]) {
self.id = id;
self.question = question;
self.choices = choices;
self.multipleResponse = multipleResponse;
self.responseCount = responseCount;
self.images = images;
}
}

struct ImageInfo: Codable {
let path: String
let url: String?
}

struct Choice: Identifiable, Codable {
var id = UUID()
Expand Down