Created
February 19, 2023 02:56
-
-
Save hassanvfx/d3d1cf8f2d9d245c6960f89aeb947136 to your computer and use it in GitHub Desktop.
Quizz-sample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// QuizzView.swift | |
// TwinChatAI | |
// | |
// Created by Eon Fluxor on 2/18/23. | |
// | |
import Foundation | |
import SwiftUI | |
struct Question:Identifiable,Hashable { | |
var id = UUID().uuidString | |
let text: String | |
let options: [String] | |
var maxAnswers:Int = 1 | |
var maxOptions:Int = 3 | |
var allowsMultipleSelection: Bool{ | |
maxAnswers > 1 | |
} | |
} | |
struct MultipleAnswerQuestionView: View { | |
struct NavContext{ | |
var canContinue:Bool | |
var continueText:String | |
var continueColor:Color | |
} | |
enum Status{ | |
case singleQuestionEmptyAnswerCannotContinue | |
case singleQuestionEmptyAnswerCanContinueInCurrentQuestion | |
case singleQuestionAnsweredCanContinueToNextQuestion | |
case multipleQuestionEmptyAnswerCanContinueInCurrentQuestion | |
case multipleQuestionAnsweredCanContinueInCurrentQuestion | |
case multipleQuestionAnsweredCanContinueToNextQuestion | |
} | |
let input: [Question] | |
// let optionsPerBatch = 3 | |
let mimuminBatchSize = 2 | |
@State private var currentQuestionIndex = 0 | |
@State private var responses:[String: Set<String>] = [:] | |
@State private var currentAnswers = Set<String>() | |
private var currentQuestion: Question? { | |
currentQuestions[safe:currentQuestionIndex] | |
} | |
private var nextQuestion: Question? { | |
currentQuestions[safe:currentQuestionIndex + 1] | |
} | |
private var currentOptions: [String] { | |
currentQuestion?.options ?? [] | |
} | |
private var currentQuestions: [Question] { | |
processQuestions(input) | |
} | |
private func nextQuestionDelta(from questions: [Question], currentIndex: Int) -> Int? { | |
guard currentIndex < questions.count else { return nil } | |
let currentQuestion = questions[currentIndex] | |
if let nextIndex = (currentIndex + 1 ..< questions.count).firstIndex(where: { questions[$0].id != currentQuestion.id }) { | |
return nextIndex - currentIndex | |
} else { | |
return nil | |
} | |
} | |
private var nextQuestionIsTheSame:Bool{ | |
currentQuestion?.id == nextQuestion?.id | |
} | |
private var nextQuestionIsDifferent:Bool{ | |
currentQuestion?.id != nextQuestion?.id | |
} | |
private var noAnswers:Bool{ | |
currentAnswers.isEmpty | |
} | |
private var canTakeMultipleAnswers:Bool{ | |
currentQuestion?.allowsMultipleSelection == true | |
} | |
private var onlyTakesSingleAnswer:Bool{ | |
currentQuestion?.allowsMultipleSelection == true | |
} | |
private var fullfilledCurrentAnswer:Bool{ | |
guard let currentQuestion = currentQuestion else{ | |
return false | |
} | |
let answers = responses[currentQuestion.id]?.count ?? 0 | |
return answers >= currentQuestion.maxAnswers | |
} | |
private var status:Status{ | |
if currentQuestion?.allowsMultipleSelection == true{ | |
if fullfilledCurrentAnswer{ | |
return .multipleQuestionAnsweredCanContinueToNextQuestion | |
} else if noAnswers{ | |
return | |
.multipleQuestionEmptyAnswerCanContinueInCurrentQuestion | |
} else { | |
return .multipleQuestionAnsweredCanContinueInCurrentQuestion | |
} | |
} else { | |
if fullfilledCurrentAnswer{ | |
return .singleQuestionAnsweredCanContinueToNextQuestion | |
} else{ | |
if nextQuestionIsTheSame{ | |
return | |
.singleQuestionEmptyAnswerCanContinueInCurrentQuestion | |
} else{ | |
return .singleQuestionEmptyAnswerCannotContinue | |
} | |
} | |
} | |
} | |
private func moveBackward(){ | |
let steps = 1 | |
currentQuestionIndex -= steps | |
currentQuestionIndex = max(0,currentQuestionIndex) | |
DispatchQueue.main.async { | |
displayCurrentAnswers() | |
} | |
} | |
private func moveForward(){ | |
let status = status | |
var step:Int | |
switch status{ | |
case .singleQuestionEmptyAnswerCannotContinue: | |
step = 1 | |
case .singleQuestionEmptyAnswerCanContinueInCurrentQuestion, | |
.multipleQuestionEmptyAnswerCanContinueInCurrentQuestion: | |
if nextQuestionIsTheSame { | |
step = 1 | |
}else{ | |
step = 0 | |
} | |
case .singleQuestionAnsweredCanContinueToNextQuestion, | |
.multipleQuestionAnsweredCanContinueToNextQuestion: | |
step = nextQuestionDelta(from: currentQuestions, currentIndex: currentQuestionIndex) ?? 0 | |
case .multipleQuestionAnsweredCanContinueInCurrentQuestion: | |
step = 1 | |
} | |
currentQuestionIndex += step | |
currentQuestionIndex = min(currentQuestionIndex, currentQuestions.count - 1) | |
DispatchQueue.main.async { | |
displayCurrentAnswers() | |
} | |
} | |
func displayCurrentAnswers(){ | |
currentAnswers = Set() | |
guard let currentQuestion = currentQuestion else {return} | |
if let existingAnswers = responses[currentQuestion.id] { | |
currentAnswers = existingAnswers | |
} | |
} | |
// tap in the options | |
private func didSelect(question:Question, option:String){ | |
selectOption(question:question, option:option) | |
DispatchQueue.main.async { | |
switch status { | |
case .multipleQuestionAnsweredCanContinueToNextQuestion, | |
.singleQuestionAnsweredCanContinueToNextQuestion: | |
moveForward() | |
default: | |
break; | |
} | |
} | |
} | |
private var navContext:NavContext{ | |
switch status{ | |
case .singleQuestionEmptyAnswerCannotContinue: | |
return NavContext(canContinue: false, continueText: "", continueColor: .black) | |
case .singleQuestionEmptyAnswerCanContinueInCurrentQuestion: | |
if nextQuestionIsTheSame { | |
return NavContext(canContinue: true, continueText: "Other >", continueColor: .black) | |
}else{ | |
return NavContext(canContinue: false, continueText: "", continueColor: .black) | |
} | |
case .multipleQuestionEmptyAnswerCanContinueInCurrentQuestion: | |
if nextQuestionIsTheSame { | |
return NavContext(canContinue: true, continueText: "More >", continueColor: .black) | |
}else{ | |
return NavContext(canContinue: false, continueText: "", continueColor: .black) | |
} | |
case .singleQuestionAnsweredCanContinueToNextQuestion: | |
return NavContext(canContinue: true, continueText: "Next", continueColor: .blue) | |
case .multipleQuestionAnsweredCanContinueToNextQuestion: | |
return NavContext(canContinue: true, continueText: "Next", continueColor: .blue) | |
case .multipleQuestionAnsweredCanContinueInCurrentQuestion: | |
if nextQuestionIsTheSame { | |
return NavContext(canContinue: true, continueText: "More >", continueColor: .black) | |
}else{ | |
return NavContext(canContinue: false, continueText: "", continueColor: .black) | |
} | |
} | |
} | |
private func didTapContinue(){ | |
if let currentQuestion = currentQuestion, | |
let nextQuestion = nextQuestion, | |
currentQuestion.id != nextQuestion.id, | |
currentAnswers.count == 0 | |
{ | |
// force answer | |
return | |
} | |
DispatchQueue.main.async { | |
moveForward() | |
} | |
} | |
private func selectOption(question:Question, option: String) { | |
if let existingAnswers = responses[question.id], | |
question.allowsMultipleSelection { | |
currentAnswers.formUnion(existingAnswers) | |
} | |
if question.allowsMultipleSelection == true { | |
if currentAnswers.contains(option) { | |
currentAnswers.remove(option) | |
} else { | |
if status == .multipleQuestionAnsweredCanContinueToNextQuestion || | |
status == .singleQuestionAnsweredCanContinueToNextQuestion { | |
currentAnswers.removeFirst() | |
} | |
currentAnswers.insert(option) | |
} | |
} else { | |
if currentAnswers.contains(option) { | |
currentAnswers.remove(option) | |
} else{ | |
currentAnswers = [option] | |
} | |
} | |
responses[question.id] = currentAnswers | |
} | |
private func processQuestions(_ questions: [Question]) -> [Question] { | |
var response: [Question] = [] | |
var previousQuestion: Question? | |
for question in questions { | |
let optionsPerBatch=question.maxOptions | |
if question.options.count <= optionsPerBatch { | |
// If the question has 3 or fewer options, add it to the response as is. | |
response.append(question) | |
previousQuestion = question | |
} else { | |
// If the question has more than 3 options, batch the options and create a new question for each batch. | |
let batchSize = min(optionsPerBatch, max(mimuminBatchSize, question.options.count / mimuminBatchSize)) | |
let numBatches = (question.options.count + batchSize - 1) / batchSize | |
for batchIndex in 0..<numBatches { | |
let startIndex = batchIndex * batchSize | |
let endIndex = min(startIndex + batchSize, question.options.count) | |
let batchOptions = Array(question.options[startIndex..<endIndex]) | |
let batchQuestion = Question(id: question.id, text: question.text, options: batchOptions, maxAnswers: question.maxAnswers) | |
if let prev = previousQuestion, batchQuestion.options.count == 1 { | |
// If the previous question exists and the current batch only has one option, append the option to the previous question. | |
var updatedOptions = prev.options | |
updatedOptions.append(contentsOf: batchQuestion.options) | |
let updatedQuestion = Question(id: prev.id, text: prev.text, options: updatedOptions, maxAnswers: prev.maxAnswers) | |
response[response.count-1] = updatedQuestion | |
} else { | |
response.append(batchQuestion) | |
previousQuestion = batchQuestion | |
} | |
} | |
} | |
} | |
return response | |
} | |
var body: some View { | |
VStack(alignment: .leading, spacing: 16) { | |
HStack{ | |
Button(action: moveBackward){ | |
Image(systemName: "chevron.left") | |
.padding() | |
} | |
} | |
if let currentQuestion = currentQuestion{ | |
Text(currentQuestion.text) | |
.font(.title) | |
.padding(.horizontal) | |
.padding(.top, 32) | |
.fixedSize(horizontal: false, vertical: true) | |
} | |
ScrollView(.vertical,showsIndicators: true){ | |
ForEach(Array(currentOptions.enumerated()), id: \.offset) { index, answer in | |
if let currentQuestion = currentQuestion{ | |
Button(action: {didSelect(question:currentQuestion, option: answer)}) { | |
Text(answer) | |
.foregroundColor(currentAnswers.contains(answer) ? .white : .primary) | |
.padding() | |
.frame(maxWidth: .infinity) | |
.background(currentAnswers.contains(answer) ? Color.blue : Color.secondary) | |
.cornerRadius(8) | |
} | |
.padding(.horizontal) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
} | |
} | |
.frame(maxHeight:.greatestFiniteMagnitude) | |
Button(action: didTapContinue) { | |
Text(navContext.continueText) | |
.padding() | |
.frame(maxWidth: .infinity) | |
.background(navContext.continueColor) | |
.cornerRadius(8) | |
.foregroundColor(.white) | |
} | |
.buttonStyle(PlainButtonStyle()) | |
.padding() | |
.opacity(navContext.canContinue ? 1 :0) | |
} | |
} | |
} | |
//struct MultipleAnswerQuestionView: View { | |
// let questions: [Question] | |
// | |
// @State private var currentQuestionIndex = 0 | |
// @State private var selectedOptions = Set<Int>() | |
// | |
// private let optionsPerBatch = 3 | |
// | |
// private var currentQuestion: Question { | |
// questions[currentQuestionIndex] | |
// } | |
// | |
// private var currentOptions: [String] { | |
// Array(currentQuestion.options.prefix(optionsPerBatch)) | |
// } | |
// | |
// private var hasMoreOptions: Bool { | |
// currentQuestion.options.count > optionsPerBatch | |
// } | |
// | |
// private var moreOptionText: String { | |
// "More options (\(currentQuestion.options.count - optionsPerBatch) more)" | |
// } | |
// | |
// private var canContinue: Bool { | |
// selectedOptions.count > 0 || currentQuestion.allowsMultipleSelection == false | |
// } | |
// | |
// private var continueButtonLabel: String { | |
// if currentQuestion.allowsMultipleSelection { | |
// return selectedOptions.count > 0 ? "Continue" : "Pick more" | |
// } else { | |
// return "Continue" | |
// } | |
// } | |
// | |
// var body: some View { | |
// VStack(alignment: .leading, spacing: 16) { | |
// Text(currentQuestion.text) | |
// .font(.title) | |
// .padding(.horizontal) | |
// .padding(.top, 32) | |
// .fixedSize(horizontal: false, vertical: true) | |
// | |
// ForEach(currentOptions.indices, id: \.self) { index in | |
// Button(action: { | |
// optionSelected(index) | |
// }) { | |
// Text(currentOptions[index]) | |
// .foregroundColor(selectedOptions.contains(index) ? .white : .primary) | |
// .padding() | |
// .frame(maxWidth: .infinity) | |
// .background(selectedOptions.contains(index) ? Color.blue : Color.secondary) | |
// .cornerRadius(8) | |
// } | |
// .padding(.horizontal) | |
// .buttonStyle(PlainButtonStyle()) | |
// } | |
// | |
// if hasMoreOptions { | |
// Button(action: { | |
// currentQuestionIndex = (currentQuestionIndex + 1) % questions.count | |
// selectedOptions = Set() | |
// }) { | |
// Text(moreOptionText) | |
// } | |
// .padding(.horizontal) | |
// .buttonStyle(PlainButtonStyle()) | |
// } | |
// | |
// Spacer() | |
// | |
// if canContinue { | |
// Button(action: { | |
// if currentQuestionIndex < questions.count - 1 { | |
// currentQuestionIndex += 1 | |
// selectedOptions = Set() | |
// } | |
// }) { | |
// Text(continueButtonLabel) | |
// .padding() | |
// .frame(maxWidth: .infinity) | |
// .background(canContinue ? Color.blue : Color.secondary) | |
// .cornerRadius(8) | |
// .foregroundColor(.white) | |
// } | |
// .padding(.horizontal) | |
// .buttonStyle(PlainButtonStyle()) | |
// } | |
// | |
// Spacer() | |
// } | |
// } | |
// | |
// private func optionSelected(_ index: Int) { | |
// if currentQuestion.allowsMultipleSelection { | |
// if selectedOptions.contains(index) { | |
// selectedOptions.remove(index) | |
// } else { | |
// selectedOptions.insert(index) | |
// } | |
// } else { | |
// selectedOptions = [index] | |
// } | |
// } | |
//} | |
// | |
//struct Question { | |
// let text: String | |
// let options: [String] | |
// let allowsMultipleSelection: Bool | |
//} | |
struct MultipleAnswerQuestionView_Previews: PreviewProvider { | |
static let questions = [ | |
Question(text: "1 What's your favorite color?", options: ["Red", "Green", "Blue", "Yellow", "Orange", "Purple"], maxAnswers: 1), | |
Question(text: "2 What's your favorite food?", options: ["Pizza", "Sushi", "Burger", "Pasta", "Salad", "Tacos"], maxAnswers: 2), | |
Question(text: "3 Which of these animals do you like the most?", options: ["Dogs", "Cats", "Birds", "Fish", "Lizards", "Rabbits", "Hamsters"], maxAnswers: 1), | |
Question(text: "4 What's your favorite type of music?", options: ["Rock", "Pop", "Hip-hop", "Classical", "Jazz", "Electronic"], maxAnswers: 2), | |
Question(text: "5 What's your favorite hobby?", options: ["Reading", "Gaming", "Drawing", "Cooking", "Watching movies", "Exercising"], maxAnswers: 1), | |
Question(text: "6 Which of these places would you like to visit the most?", options: ["New York", "Tokyo", "Paris", "Rio de Janeiro", "Sydney", "Dubai", "Cape Town"], maxAnswers: 2), | |
Question(text: "7 What's your favorite sport?", options: ["Football", "Basketball", "Tennis", "Swimming", "Running", "Golf"], maxAnswers: 1), | |
Question(text: "8 Which of these languages would you like to learn?", options: ["Spanish", "French", "German", "Mandarin", "Arabic", "Russian"], maxAnswers: 2), | |
Question(text: "9 What's your favorite type of movie?", options: ["Action", "Comedy", "Drama", "Thriller", "Horror", "Science fiction"], maxAnswers: 1), | |
Question(text: "10 What's your favorite season?", options: ["Spring", "Summer", "Fall", "Winter"], maxAnswers: 1) | |
] | |
static var previews: some View { | |
MultipleAnswerQuestionView(input: questions) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment