Skip to content

Instantly share code, notes, and snippets.

@hassanvfx
Created February 19, 2023 02:56
Show Gist options
  • Save hassanvfx/d3d1cf8f2d9d245c6960f89aeb947136 to your computer and use it in GitHub Desktop.
Save hassanvfx/d3d1cf8f2d9d245c6960f89aeb947136 to your computer and use it in GitHub Desktop.
Quizz-sample
//
// 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