0

I'm in the process of writing an app to display and navigate through a series of questions and answers using Swift. I currently have a dictionary of questions and associated answers which I then work through using .map to create an array of 'flashcard' objects (each holds a question and it's associated answer).

When I then go to access these objects from the array, for some reason the 0th item returned appears to be the second item in my dictionary rather than the first (e.g. 'question 2'). Can someone explain why this is? This is the class which creates the array of data and provides the means to return items to the relevant view controller:

    import Foundation

class Deck {
    // Create constant to hold the deck of cards
    private var cards: [Flashcard]
    var bookmark: Int // Remembers which card we are on


    init() {
        let cardData = ["Question 1" : "Answer 1",
                        "Question 2" : "Answer 2",
                        "Question 3" : "Answer 3"]

        cards = cardData.map { Flashcard(question: $0, answer: $1) }
        bookmark = 0 // Start with bookmark on first item in deck
    }

    // Getter method to return current card
    public func getCurrentCard() -> Flashcard {
        print("Bookmark set to \(bookmark)")
        return cards[bookmark]
    }

    // Getter method to return the next card in the deck
    public func getNextCard() -> Flashcard {
        bookmark += 1
        print("Bookmark set to \(bookmark)")
        return cards[bookmark]
    }

    // Getter method to return previous card in deck
    public func getLastCard() -> Flashcard {
        bookmark -= 1
        print("Bookmark set to \(bookmark)")
        return cards[bookmark]
    }
}

Then this is my view controller where I request the objects to be displayed:

import UIKit

class QuestionController: UIViewController {

  // Outlet for the 'question' label
    @IBOutlet weak var questionTextView: UITextView!

    // Outlet for the next button
    @IBAction func nextQuestion(_ sender: UIButton) {
        flashcard = deck.getNextCard()
        questionTextView.text = flashcard?.question

    }

    // Outlet for last button
    @IBAction func lastQuestion(_ sender: UIButton) {
        flashcard = deck.getLastCard()
        questionTextView.text = flashcard?.question
    }



    // variable to hold current flashcard
    var flashcard: Flashcard?
    let deck = Deck() // Holds deck of data

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        flashcard = deck.getCurrentCard()
        questionTextView.text = flashcard?.question

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }



// Prepare flashcard item to be passed to the answer controller before segue
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let answerController = segue.destination as? AnswerController {
            answerController.flashcard = flashcard
        }
    }

}

When I run the app the first question displayed is "Question 2" when I was expecting it to be "Question 1"...?

laurie
  • 965
  • 1
  • 11
  • 21
  • Related: [Ordered dictionary in Swift](http://stackoverflow.com/questions/30969688/ordered-dictionary-in-swift). – Martin R May 14 '17 at 11:52

1 Answers1

3

The problem is that the order of key-value pairs in a Dictionary is unspecified – it's completely implementation dependant, and not guaranteed to be the same as the order in which the key-value pairs appear in the dictionary literal you use to create the dictionary.

A simple solution in your case is to use, not a dictionary, but a DictionaryLiteral – which maintains the order of key-value pairs:

init() {
    let cardData: DictionaryLiteral = ["Question 1" : "Answer 1",
                                       "Question 2" : "Answer 2",
                                       "Question 3" : "Answer 3"]

    cards = cardData.map { Flashcard(question: $0, answer: $1) }
    bookmark = 0 // Start with bookmark on first item in deck
}

This is because under the hood, DictionaryLiteral is nothing more than an array of key-value tuples. Because of this, you'll notice that it behaves very differently to a Dictionary in that, as well as maintaining order of key-value pairs:

  • It has a zero-based Int index that you can subscript with
  • It doesn't require the keys to be Hashable
  • It allows duplicate keys
  • Key lookup takes place in linear (not constant) time

Its main use therefore is really just a bit of transient syntactic sugar (allowing the use of a dictionary literal) before you then pack the key-value pairs into your actual data structure – exactly how we're using it here.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thanks Hamish, I wondered whether it could have been something to do with that but was thinking maybe it would randomly start from a different object each time the app was run whereas it always starts with the second item. – laurie May 14 '17 at 11:57
  • @laurie Happy help :) It's worth noting that not even the order of key-value pairs in a `Dictionary` between executions of the *same* program is to be relied upon – as the `Hashable` protocol gives no guarantee that the hash value returned for the same instance across different executions will be stable (and this is specifically noted in the documentation). – Hamish May 14 '17 at 12:02
  • Hello @Hamish, do you think that an array of custom model would be better? btw, finally I saw a useful usage for `DictionaryLiteral` thanks for the answer. – Ahmad F May 14 '17 at 13:28
  • @AhmadF An array of a custom model is exactly what OP is making in this case (a `[Flashcard]`) – the use of `DictionaryLiteral` here is really only for syntactic sugar so that OP doesn't have to write out an array literal repeating "`Flashcard(question: ... , answer: ...)`" for each element. Instead we only have to state the question and answer string pairs. – Hamish May 14 '17 at 13:31