2
\$\begingroup\$

I am creating a true or false quiz game and when the user chooses an answer a tick or cross will appear on the screen telling the user whether they are right or wrong and then automatically bring the next question in.

Currently I am using 2 almost identical functions that draw the next question from my question array (one for each button). It appears to work but seems very cluttered and illogical. Is there a more elegant way to code the logic to this?

Also, the concept of a previous question button has caused a lot of issues. Through a lot of research I am still not sure how I would incorporate one into the program.

@IBAction func trueButton(sender: AnyObject) {   
    delay(2) {
        nextQuestionTrue()
    }  
}

@IBAction func falseButton(sender: AnyObject) {
    delay(2) {
        nextQuestionFalse()
    }
}

func nextQuestionTrue() -> QuizQuestion {
    // initialize current question

    var currentQuestion: QuizQuestion = QuizQuestion(question: "", answer: false, explanation: "")

    if currentIndex < questions.count {
      currentQuestion =  questions[currentIndex]
    }

    if currentQuestion.answer == true {
        answerLabel.text = "correct"

    } else if currentQuestion.answer == false {
        answerLabel.text = "Incorrect"
    }

    questionLabel.text = currentQuestion.question
    currentIndex++
    return currentQuestion
}

func nextQuestionFalse() -> QuizQuestion {
    // initialize current question

    var currentQuestion: QuizQuestion = QuizQuestion(question: "", answer: false, explanation: "")

    if currentIndex < questions.count {
        currentQuestion =  questions[currentIndex]
    }

    if currentQuestion.answer == false {
        answerLabel.text = "correct"

    } else if currentQuestion.answer == true {
        answerLabel.text = "Incorrect"
    }

    questionLabel.text = currentQuestion.question
    currentIndex++
    return currentQuestion
}
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Personally I would combine the two @IBActions into a single one and identify the pressed button from the sender object.


I would also recommend you to look into the Ternary Conditional Operator, which can make your simple if statements much cleaner, like the following:

if currentQuestion.answer == true {
    answerLabel.text = "correct"
} else if currentQuestion.answer == false {
    answerLabel.text = "Incorrect"
}

// The previous is exactly the same as the following:
answerLabel.text = (currentQuestion.answer == true) ? "correct" : "Incorrect"

You can read more about the Ternary Conditional Operator here.


You can see my result from trying to recreate your current setup as inspiration (FYI, my buttons have the titles "True" and "False", which I make use of to turn them into booleans):

//
//  ViewController.swift
//  TrueFalseQuiz
//
//  Created by Stefan Veis Pennerup on 06/06/15.
//  Copyright (c) 2015 Kumuluzz. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    // MARK: - Question models
    // Notice: This is not the best practice for storing the model in terms of the MVC pattern, 
    // this is just for illustration purposes.
    let questions = [
        QuizQuestion(question: "Do I like coffee?", answer: true, explanation: "Because it's awesome!"),
        QuizQuestion(question: "Is bacon god's gift to mankind?", answer: true, explanation: "Because it's awesome!"),
        QuizQuestion(question: "Should I take a nap right now?", answer: false, explanation: "You gotta review some code!")]
    var currentQuestionIndex = 0

    // MARK: - Storyboard outlets

    @IBOutlet weak var questionLabel: UILabel!
    @IBOutlet weak var answerLabel: UILabel!

    // MARK: - Lifecycle methods

    override func viewDidLoad() {
        super.viewDidLoad()

        // Initializes the first question
        self.answerLabel.text = ""
        self.questionLabel.text = questions[currentQuestionIndex].question
    }

    // MARK: - Storyboard actions

    /**
    The target action for both the true false buttons
    */
    @IBAction func answerPressed(sender: UIButton) {
        // Exits if there aren't any questions left
        if currentQuestionIndex >= questions.count { return }

        // Retrieves the user's answer and figures out if it correct
        let userAnswer = sender.currentTitle
        let isAnswerCorrect = userAnswer?.toBool() == questions[currentQuestionIndex].answer

        // Prints appropiate message
        answerLabel.text = (isAnswerCorrect) ? "Correct" : "Incorrect"

        // Updates with a new question
        currentQuestionIndex++
        let isThereAnyQuestionsLeft = currentQuestionIndex < questions.count
        questionLabel.text = (isThereAnyQuestionsLeft) ? questions[currentQuestionIndex].question : "No more questions"        
    }
}

// Extends the standard String class to convert a string to a boolean
extension String {
    func toBool() -> Bool? {
        switch self {
        case "True", "true", "yes", "1":
            return true
        case "False", "false", "no", "0":
            return false
        default:
            return nil
        }
    }
}

Could you describe a bit more about the "previous question" button? Should it replace the current question? Should it show the user's answer or just start over with the question?


UPDATE 1:

This update is concerning the "previous question" funcionality outlined by OP in the comments of this answer.


Regarding next and back:

I would add two new buttons (a "next" and "back" button, surprise, surprise) and create two separate storyboard actions for each of them. Each method would respectively increment or decrement the current question index. Both of the methods would then update the currently displayed question.

// MARK: - Next/back methods

@IBAction func nextPressed(sender: UIButton) {
    currentQuestionIndex++
    if currentQuestionIndex == questions.count { currentQuestionIndex = 0 }
    updateCurrentQuestion()
}

@IBAction func backPressed(sender: UIButton) {
    currentQuestionIndex--
    if currentQuestionIndex < 0 { currentQuestionIndex = questions.count - 1 }
    updateCurrentQuestion()
}

func updateCurrentQuestion() {
    answerLabel.text = ""
    questionLabel.text = questions[currentQuestionIndex].question
}

Regarding submitting all:

In the code outline aboved there are 2 main varibles related to your questions model:

let questions: [QuizQuestion]
var currentQuestionIndex: Int

To handle the feature for storing the user's answers, then I would add another property to your QuizQuestion model to hold the user's answers. This means your model would look something similar to the following:

//
//  QuizQuestion.swift
//  TrueFalseQuiz
//
//  Created by Stefan Veis Pennerup on 06/06/15.
//  Copyright (c) 2015 Kumuluzz. All rights reserved.
//

import Foundation

class QuizQuestion {

    let question: String!
    let answer: Bool!
    let explanation: String!
    var usersAnswer: Bool?

    init(question: String, answer: Bool, explanation: String) {
        self.question = question
        self.answer = answer
        self.explanation = explanation
    }
}

Since usersAnswer is an Optional, then it will initially be nil. Whenever the method answerPressed is called, then you should add the following line to store the answer, thus overwriting nil: let userAnswer = sender.currentTitle questions[currentQuestionIndex].usersAnswer = userAnswer?.toBool()

I would then add a new button called "finish" or "submit all" and connect the following storyboard action, which determines if all the questions have been answered before continuing.

// MARK: - Finishing 

@IBAction func submitAll(sender: UIButton) {
    let hasAnsweredAllQuestions = questions.reduce(true) { (x, q) in x && (q.usersAnswer != nil) }
    println("has user answered all questions?: \(hasAnsweredAllQuestions)")
}

UPDATE 2:

Explanation of reduce for OP.


reduce can be quite hard to grasp initially, but once you understand it, then it can be super powerful. In short, it takes all the values of an array and compute it into a single value (could be an Int, Bool or something else). You can read more about it here: https://stackoverflow.com/questions/28283451/ios-swift-reduce-function.

The line

let hasAnsweredAllQuestions = questions.reduce(true) { (x, q) in x && (q.usersAnswer != nil) } 

could be expanded to something similar the following:

var hasAnsweredAllQuestions = true
for q in questions {
    if (q.usersAnswer != nil) {
        hasAnsweredAllQuestions = true
    }
    else {
        hasAnsweredAllQuestions = false
        break;
    }
}

As you can see it simply loops over all the questions and checks if the user has given an answer (if the answer is not nil).

Other functions which are equally powerful as reduce are map and filter which you can try to do more research on.


Regarding checking the user's answer with the correct answer, then you could consider using map, but I'll leave you with that hint for now ;)

\$\endgroup\$
7
  • \$\begingroup\$ The previousQuestion button would remove a users and replace the current question. Ultimately I would love the user to "lock in" an answer for the question then pretty next question or previous question to go through the questions and then submit all the answers. But I feel I do not have the expertise to understand how to accommplish that. I was thinking through key pairs and giving each answer a key value pair to identify with. \$\endgroup\$
    – simlimsd3
    Jun 6, 2015 at 11:44
  • \$\begingroup\$ I'm not sure I completely understand your comment about the previous question feature, but here's what I understood: You have a series of questions (for example 10) which is presented to the user. The user should be able to go back and forth between all of the questions and answer them, but the user won't receive the result until some button called "submit all" is pressed. Then the user will receive a total score based on all those previous answers at once. Is this correct? \$\endgroup\$
    – Kumuluzz
    Jun 6, 2015 at 12:05
  • \$\begingroup\$ Yeah thats it! - Sorry I will try and explain more clearly. The previous button should bring the user to the previous question. I would also implement a next button that would do the opposite and bring the user to the next question. It wouldnt delete there answer, just give them another chance to try the question if they wish. Thank you very much for your help. \$\endgroup\$
    – simlimsd3
    Jun 6, 2015 at 12:09
  • \$\begingroup\$ Hi again. Just wondering if you could help me understand this line "let hasAnsweredAllQuestions = questions.reduce(true) { (x, q) in x && (q.usersAnswer != nil) }" by explaining a bit more of how it works. Also in your opinion what is the best way to check the users answers and compare it to the correct answers for a summary? \$\endgroup\$
    – simlimsd3
    Jun 6, 2015 at 16:15
  • \$\begingroup\$ Hello, Ive attached a github link to an idea Ive been working on to check all the submitted answers. I have struggled to find a way to use map to change the bools into strings I can present to the user. gist.github.com/simlims/9e2814bf6e59292e6a18 \$\endgroup\$
    – simlimsd3
    Jun 8, 2015 at 0:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.