Displaying random questions by looping through a dictionary - ios

I started to learn swift by trying to make a quiz game. But i'm stuck at trying to display random questions from an dictionary. The reason why i use a dictionary is because the quiz i'm working on is based on chemistry elements which has different symbols.
So Based on the selected difficulty i created four different dictionaries:
//Defining the dictionaries per difficulty
var easy: [String: String] = [
"H": "Waterstof",
"He": "Helium",
"Li": "Lithium",
"B": "Boor",
"C": "Koolstof",
"O": "Zuurstof",
"Ne": "Neon",
"Al": "Aliminium",
"Si": "Silicium",
"K": "Kalium",
"Fe": "Ijzer",
"Ni": "Nikkel",
"Zn": "Zink",
"Cd": "Cadmium",
"I": "Jood"
]
var normal: [String: String] = [
"N": "Stikstof",
"F": "Fluor",
"Na": "Natrium",
"Mg": "Magnesium",
"P": "Fosfor",
"CI": "Chloor",
"Ar": "Argon",
"S": "Zwavel",
"Ca": "Calcium",
"Cu": "Koper",
"Au": "Goud",
"Br": "Broom",
"Ag": "Zilver",
"Pt": "Platina",
"Ba": "Barium"
]
and so on.. (hard, extreme)
This is my viewDidLoad() method:
override func viewDidLoad() {
switch (self.selectedDifficultyByUser){
case "Makkelijk":// Means 'easy' in dutch
//Assigning the easy dictionary to a temporary dictionary
self.temp = self.easy
case "Normaal":
//Assigning the normal dictionary to a temporary dictionary
self.temp = self.normal
case "Moeilijk":
//Assigning the hard dictionary to a temporary dictionary
self.temp = self.moeilijk
case "Extreem":
//Assigning the extreme dictionary to a temporary dictionary
self.temp = self.extreem
default:
println("Something went wrong")
}
super.viewDidLoad()
self.answers = [self.btn1, self.btn2, self.btn3, self.btn4]
pickRandomElement()
randomizeAnswers()
let updateTime : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: updateTime, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
}
As you can see i'm assigning all my buttons to the array 'answers' which i will be looping through soon.
The function pickRandomElement will then be called, which looks like this:
func pickRandomElement() -> () {
//First here we make sure that the 'answerOptions' Array is cleared,
//Because it could cause that the correct answer won't be showed.
answerOptions = [String]()
if self.selectedDifficultyByUser == "Makkelijk" {
let index: Int = Int(arc4random_uniform(UInt32(easy.count)))
let symbol = Array(easy.keys)[index]
let element = Array(easy.values)[index]
self.currentQuestion = symbol
self.correctAnswer = element
//Assign the correctAnswer to the answerOptions array
self.answerOptions.append(element)
//Show the question to the user
self.questionLabel.text = self.currentQuestion
//remove the correctanswer from the dictionary, to
//make sure that the answers won't be duplicated
self.easy[symbol] = nil
self.easy[element] = nil
for (var i = 0; i < 3; i++) {
let randomIndex: Int = Int(arc4random_uniform(UInt32(easy.count)))
let optionSymbol = Array(easy.keys)[randomIndex]
let optionElement = Array(easy.values)[randomIndex]
self.answerOptions.append(optionElement)
//Removing 'optionSymbol' and 'optionElement' from the array
//to prevent duplicated answer
self.easy[optionElement] = nil
self.easy[optionSymbol] = nil
}
self.easy = self.temp //Filling the 'easy' array with the temporary array
}
}
The problem i'm having is in the for loop shown above. I'm looping three times to pick random elements and show them to the user. And during this process i'm deleting the randomly chosen elements from the (easy) array to prevent duplicate answer.
And because i'm removing those elements from the easy array i'm assigning the temporary array, which i created in the begin, back to the easy array.
If i don't do this, the whole easy array will be empty after three rounds or so.
If i do the opposite i will get an infinte loop.
Can someone please put me in the right direction if i'm doing this the wrong way or help me out of this problem?
Thanks in advance!
UPDATED
Firo's solution worked perfectly for me. But i'm know faced with another bug. Namely, the pickRandomElement function returns sometimes the asked question more than once. I tried to make another dictionary and put the asked questions in there and check to see if that question has already been asked. However, as the number of questions answered increases, this would require a linearly increasing amount of lookups for each question asked, until all lookups is reached. Does someone know how i can take care of this problem? –

So without all of your code I can only give you an educated guess as to a good way of handing your infinite loop situation with your current code.
An easy way to prevent an infinite loop here is to verify you still have remaining questions you want to ask. Keep an instance value to check how many questions you still want to ask.
var remainingSteps = 5
Then when the user answers the correct question, check it before presenting the next question
func userAnsweredQuestionCorrectly() {
if self.remainingSteps > 0 {
// Present next question
// Decrement remaining steps
self.remainingSteps--
else {
// Done asking questions, back to menu (or whatever is next)
}
}

Firo's solution is awesome. This one might help you with getting a unique question each time to avoid repetition. Let me know here or on codementor if you have any questions.

Related

index(where:) method in swift is producing the wrong index

I have received an array index out of range error. I have two arrays cardsCurrentlyInPlay and currentCardsSelected. Every card that is in this game has a unique ID. I am attempting to find find the index of the card in cardsCurrentlyInPlay whose cardID matches the cardID of the card in currentCardsSelected. I am doing this by using the index(where:) method that takes a closure. My closure just checks if the IDs match, they obviously match because I am using the ! to unwrap them and the app does not crash there. It seems as though the index(where:) method is returning the wrong index. I have looked at this for hours and I do not understand whats going on.
Heres the code:
let indexOfFirstCard = cardsCurrentlyInPlay.index(where: ({($0?.cardID == currentCardsSelected[0].cardID)}))!
let indexOfSecondCard = cardsCurrentlyInPlay.index(where: ({($0?.cardID == currentCardsSelected[1].cardID)}))!
let indexOfThirdCard = cardsCurrentlyInPlay.index(where: ({($0?.cardID == currentCardsSelected[2].cardID)}))!
if deck.isEmpty && selectedCardsMakeASet() {
/* Remove the old cards */
cardsCurrentlyInPlay.remove(at: indexOfFirstCard)
cardsCurrentlyInPlay.remove(at: indexOfSecondCard)
cardsCurrentlyInPlay.remove(at: indexOfThirdCard) // where code is blowing up
currentCardsSelected.removeAll()
/* Return indicies of cards to clear from the UI */
return .deckIsEmpty(indexOfFirstCard, indexOfSecondCard, indexOfThirdCard)
}
The index you’re getting is correct when your get it, but it becomes wrong when you remove other cards. Consider:
var a = ["x", "y", "z"]
let indexOfX = a.index(of: "x")! // returns 0
let indexOfZ = a.index(of: "z")! // returns 2
a.remove(at: indexOfX) // removes "x"; now a = ["y", "z"]
a.remove(at: indexOfZ) // index 2 is now out of bounds
You could interleave the calls to index(of:) and remove(at:), but a better approach would be to remove all three cards in a single pass, something like this:
let selectedCardIDs = currentCardsSelected.map { $0.cardID }
cardsCurrentlyInPlay = cardsCurrentlyInPlay.filter { card in
!selectedCardIDs.contains(card.cardID)
}
Note that this has the added benefit of avoiding the force unwrap, a sign of sounder logic.
This is because of out of bounds.
After the first two code excuted.
cardsCurrentlyInPlay.remove(at: indexOfFirstCard) &
cardsCurrentlyInPlay.remove(at: indexOfSecondCard)
there is only one element in the cardsCurrentlyInPlay .
Then if you excute cardsCurrentlyInPlay.remove(at: indexOfThirdCard), the program will crash.

If statements with arrays in swift

I am having some trouble with my swift code. I want to make an endless game similar to the line zen, where a random node appears from the top. I used this code to help use the randomizer:
let randomWallNameIndex = Int(arc4random_uniform(2))
let wallNames = ["obsticle #1", "obsticle #2"]
//can also be [1, 2,]
if wallNames == "obsticle #1"{
//insert code here
}
Although, I'm having trouble using the if statement to tell whether a specific number was selected and I can spawn that certain node.
Can someone find the solution?
let randomWallNameIndex = Int(arc4random_uniform(2))
let wallNames = ["obsticle #1", "obsticle #2"]
//can also be [1, 2,]
let wall = wallNames[randomWallNameIndex]
if wall == "obsticle #1"{
//insert code here
}
Just add a variable that holds the string from the array at that index and use that variable in the if statement.

App crashes if playlist count = 0 (Do empty playlists have a persistentid?)

My app is crashing if a playlist is empty (no songs). My app works for all non-empty playlists. It seems like there isn't a persistentid for an empty playlist, but I think I am wrong on that.
let qryPlaylists = MPMediaQuery.playlistsQuery()
var selectedPlaylistTitle: String!
var selectedPlaylistPersistentID: UInt64!
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath: NSIndexPath = playlistsTableView.indexPathForSelectedRow!
let rowItem = qryPlaylists.collections![indexPath.row]
let playlistSize = rowItem.count
print("playlistSize = ", playlistSize)
selectedPlaylistTitle = rowItem.valueForProperty(MPMediaPlaylistPropertyName) as! String
print("selectedPlaylistTitle = ", selectedPlaylistTitle)
// Will crash on this next line if the playlistSize = 0
selectedPlaylistPersistentID = rowItem.items[0].persistentID
// If I use following line instead, it will crash on any playlist
// selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
// This line will never be printed
print("selectedPlaylistPersistentID = ", selectedPlaylistPersistentID)
}
Thanks in advance for any help!
If an array such as items is empty, it has no index 0 and will crash if you refer to it, with an out-of-bounds error. So if you don't want to crash, don't do that. You already know that items is empty, because rowItem.count told you so; as you said yourself, playlistSize is 0 in that case.
A simple-minded way to look at it is this: the largest legal index in an array is one less than its count.
Another issue you asked about is that this line always crashes:
selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
The problem here is that you are apparently using Swift 2.x. (You should have said that in your question; I deduce it, though, from the fact that valueForProperty has not changed to value(forProperty:), which is what it is called in Swift 3.)
In Swift 2, you cannot cast directly down to UInt64. (I am surprised that the compiler does not draw your attention to this fact with a warning.) Thus, the cast fails and you crash. You need to cast down to NSNumber and then take that NSNumber's unsignedLongLongValue.
And while you are doing this, you really should stop using exclamation marks in your code. When I say "cast down", I mean "cast down safely". My own Swift 2 code for doing this sort of thing looks like this:
if let id = (rowItem.valueForProperty(MPMediaItemPropertyAlbumPersistentID) as? NSNumber)?.unsignedLongLongValue {
// ...
}

Core Data in Swift: Comparing objects in a for loop

I'm trying to update the user's music listen history by checking to see if any MPMediaItemPropertyPlayCounts have changed since the last check. Right now, upon each check, I'm querying the entire iPod library and comparing their existing play counts already in Core Data. If current play count != play count stored in Core Data, we do something with that song, since we know the user has listened to it recently.
In my code, I'm struggling to loop through all iPod songs and search for corresponding Core Data objects simultaneously. The code below prints out way too many lines. How do I search for objects in Core Data in a for loop?
class func checkiPodSongsForUpdate() {
var appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var context: NSManagedObjectContext = appDel.managedObjectContext!
var newSong = NSEntityDescription.insertNewObjectForEntityForName("IPodSongs", inManagedObjectContext: context) as! NSManagedObject
var request = NSFetchRequest(entityName: "IPodSongs")
request.returnsObjectsAsFaults = false
var results = context.executeFetchRequest(request, error: nil)
let query = MPMediaQuery.songsQuery()
let queryResult = query.collections as! [MPMediaItemCollection]
for song in queryResult {
for song in song.items as! [MPMediaItem] {
request.predicate = NSPredicate(format: "title = %#", "\(song.valueForProperty(MPMediaItemPropertyTitle))")
for result: AnyObject in results! {
if let playCount = result.valueForKey("playCount") as? String {
if playCount != "\(song.valueForProperty(MPMediaItemPropertyPlayCount))" {
println("\(song.valueForProperty(MPMediaItemPropertyTitle)) has a new play count.")
} else {
println("User hasn't listened to this song since last check.")
}
}
}
}
}
}
The immediate problem is that you assign a value to request.predicate, but doing so has no effect. A fetch request predicate only has effect if you assign it before doing the fetch. It becomes part of the fetch, and the fetch results only include objects that match the predicate.
There are a couple of possibilities for fixing this specific problem:
Do your Core Data fetch in the loop after assigning the predicate, inside the loop. This would get the results you expect. However it will also be extremely inefficient and won't scale well. Fetch requests are relatively expensive operations, so you'll probably find that you're spending a lot of time doing them.
Instead of assigning a value to request.predicate, filter the results array in memory by doing something like
let predicate = NSPredicate(format: "title = %#", "\(song.valueForProperty(MPMediaItemPropertyTitle))")
let currentResults = results.filteredArrayUsingPredicate(predicate)
That will get you an array of only the songs matching the predicate. This will be faster than option #1, but it still leaves the major problem that you're fetching all of the songs here. This potentially means loading a ton of managed objects into memory every time you call this function (suppose the user has, say, 50,000 songs?). It's still not a good solution. Unfortunately I don't know enough about MPMediaQuery to recommend a better approach.
Either of these should fix the immediate problem, but you need to address the more significant conceptual issue. You're comparing two potentially large collections looking for a single match. That's going to be a lot of CPU time or memory or both no matter how you approach it.
Found my own solution. Looping through two arrays can get expensive with enough objects in each array, just as #Tom said. I decided to loop through arrayOne and inside that loop, call a custom function that looks for the given key/value pair in arrayTwo.
// Custom function that returns Bool if arrayTwo contains a given key/value pair from inside the arrayOne loop
func arrayContains(array:[[String:String]], #key: String, #value: String) -> Bool {
for song in array {
if song[key] == value {
return true
}
}
return false
}
Big thanks to #vacawama for the help here: Find key-value pair in an array of dictionaries

How do I access an Array stored in a Dictionary in Swift?

This is how I created my Dictionary with the Array:
#IBAction func didTapSaveButton(sender: UIButton) {
var hooObject = Dictionary<String, [String]>()
hooObject["\(dayLabel.text)"] = ["\(fromTimeLabel.text) - \(toTimeLabel.text)", "\(costField.text)"]
NSLog("RESULT: \(hooObject)")
}
NSLog result:
RESULT: [Optional("Wednesday"): [Optional("9:00 PM") - Optional("3:00 PM"), 40.00]]
I've tried this:
#IBAction func didTapSaveButton(sender: UIButton) {
var hooObject = Dictionary<String, [String]>()
hooObject["\(dayLabel.text)"] = ["\(fromTimeLabel.text) - \(toTimeLabel.text)", "\(costField.text)"]
var res = hooObject["Wednesday"]
NSLog("RESULT: \(res)")
}
This returns nil which doesn't make sense to me. Wednesday is the stored key so I would have thought it would be enough to return the array.
What I'm trying to do:
Basically I have a table with 7 sections and each represent a day. In each day I'm storing time slots with a cost for booking an activity in that slot.
When I use numberOfRowsInSection I will be able to use the Dictionary key to get the number of rows (time slots) for a particular day that will be shown in the tableView. This was the easiest way I could think to do it with NOSql.
I store the dictionaries in a column in parse.com and access that dictionary in my tableView.
Anyway I don't understand why I can't seem to access the array with the key. I could easily go and use an NSMutableDictionary but I'm using Swift for all my projects as a learning experience.
Wondering if it would be better to use a Dictionary with Dictionaries e.g. ["fromTime":value, "toTime":value, "cost":value]
Will appreciate any help given.
Thanks for your time.
The problem comes from using string interpolation on dayLabel.text -- since that's a String? and not a String, you end up with "Optional(Wednesday)" as your key instead of just "Wednesday". You need to unwrap it, like this:
#IBAction func didTapSaveButton(sender: UIButton) {
var hooObject = Dictionary<String, [String]>()
if let day = dayLabel.text {
hooObject["\(day)"] = ["\(fromTimeLabel.text) - \(toTimeLabel.text)", "\(costField.text)"]
NSLog("RESULT: \(hooObject)")
}
}
You'll probably need to do something similar with the from and to time fields to get the values you want.
My answer:
#IBAction func didTapSaveButton(sender: UIButton) {
var hooObject = Dictionary<String, [String]>()
hooObject["\(dayLabel.text!)"] = ["\(fromTimeLabel.text) - \(toTimeLabel.text)", "\(costField.text)"]
var res = hooObject["Wednesday"]
NSLog("RESULT: \(res)")
}
I had to change dayLabel.text to dayLabel.text!
Those exclamation marks really baffle me. At first I thought they were like pointers because they can't be used on Int, Float etc (so I think).
But the fact I need one at the end of dayLabel.text and not my other labels baffles me even more. I guess in time I will have my ah hah moment. I did read a few posts on here but still don't quite get it.

Resources