Search code examples
swiftgameplay-kit

How do I use GKRandomSource from Swift's Gameplaykit to shuffle questions


I'm new to Swift and iOS development and am doing a number of online courses. In one course I completed an iPhone quiz app. All the questions and potential answers are loaded from a .json file and the code we worked through randomly selects a question for the user to answer. This works fine except that over time questions will repeat, so I've been looking at ways to prevent this.

After a few failed attempts to get the app to store the random selections and remove them from the overall pool of remaining questions, I realised that the more efficient approach would be to just shuffle the questions initially and then ask them one by one.

So, I did some digging around and came across what's called a Fisher-Yates shuffle and have since been trying to find a way to achieve this. I then found a reference to using GKRandomSource from Swift's Gameplaykit to shuffle things, so I've been trying to use this to do the trick and can't seem to get it to work.

Below is what I've tried so far in terms of code etc.

All questions and potential answer choices are stored in a .json file as such:

{
        "id" : "1",
        "question": "Earth is a:",
             "answers": [
            "Planet",
            "Meteor",
            "Star",
            "Asteroid"
          ],
          "difficulty": "1"
      }

I use the following code to load the .json file:

func loadAllQuestionsAndAnswers()
{
    let path = NSBundle.mainBundle().pathForResource("content", ofType: "json")
    let jsonData : NSData = NSData(contentsOfFile: path!)!
    allEntries = (try! NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)) as! NSArray
    //println(allEntries)

}

And below is the code I'm using to try and achieve the shuffle of all questions:

var shuffledNumber : Int! = 0

    loadAllQuestionsAndAnswers()

    if #available(iOS 9.0, *) {
        let shuffledNumber = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(allEntries as NSArray as [AnyObject])
    } else {
        // Fallback on earlier versions
    }
    LoadQuestion(shuffledNumber)

I use a label to display the question and four images to display the potential answers, using the following code:

func LoadQuestion(index : Int)
{
    let entry : NSDictionary = allEntries.objectAtIndex(index) as! NSDictionary
    let question : NSString = entry.objectForKey("question") as! NSString
    let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray

    //println(question)
    //println(arr)

    labelQuestion.text = question as String

    let indices : [Int] = [0,1,2,3]
    //let newSequence = shuffle(indices)
    let newSequence = indices.shuffle()
    var i : Int = 0
    for(i = 0; i < newSequence.count; i++)
    {
        let index = newSequence[i]
        if(index == 0)
        {
            // we need to store the correct answer index
            currentCorrectAnswerIndex =  i

        }

        let answer = arr.objectAtIndex(index) as! NSString
        switch(i)
        {
        case 0:
            buttonA.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 1:
            buttonB.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 2:
            buttonC.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        case 3:
            buttonD.setTitle(answer as String, forState: UIControlState.Normal)
            break;

        default:
            break;
        }



    }
    buttonNext.hidden = true
    // we will need to reset the buttons to reenable them
    ResetAnswerButtons()

}

Finally, I use the following code to present the user with a 'Next' button after they've answered a question:

@IBAction func PressedButtonNext(sender: UIButton) {
    print("button Next pressed")
        if #available(iOS 9.0, *) {
            _ = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(allEntries as [AnyObject])
        } else {
            // Fallback on earlier versions
        }
        LoadQuestion(shuffledNumber)

Now, while I think this should all work, it's not. While the app loads fine, it always presents the user with the very first question from the .json file and keeps repeating the same question every time you tap on the Next button.

How do I get this to shuffle all the questions and then just ask them in order one by one? Let me know if I need to include more of the code.


Solution

  • arrayByShufflingObjectsInArray gives you a new array that is shuffled. Therefore you should be calling it once, and moving an index along it when PressedButtonNext is called. Given you have declared

        var shuffledNumber : Int! = 0
    

    And then effectively redeclared

        let shuffledNumber = ...
    

    It's not surprising it's not working. I suspect you are not running under IOS 9, so you're not getting a compilation error (although I have not used #available before).

    You should declare the following instance variables:

        var shuffledQuestions: [AnyObject]!
        var nextQuestion = -1
    

    And then in loadAllQuestionsAndAnswers, you should include

        shuffledQuestions = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(...
    

    And in PressedButtonNext

        nextQuestion++
        LoadQuestion(nextQuestion)
    

    And then change your LoadQuestion to get entry from the shuffled questions:

        let entry : NSDictionary = shuffledQuestions[index] as! NSDictionary
    

    Have a look at this example here