textual questionnaire system - swift 3 - swift

This question is about building a textual questionnaire system using swift 3.
Assuming we have (n) questions, the user will be asked one question at the time, replying with text (using UITextField).
if the answer is something that is expected (YES/NO for example) the code will carry on to the next question, otherwise it will try to give some more elaborate question with possible answers (see code below).
The idea is having some kind of an array of dictionaries, each dictionary (questionItem) contains a single question/answer data...
Question: even if the following code works fine, I feel that this is not the most scalable nor elegant solution. Thus my question is more about your ideas regarding code design.
how can one make a code such as this - that is scalable for multiple questions.
static var questionItem = [
"question": "Is the sky blue?",
"answer": "yes",
"afterAnswer": "question #2",
"explanation": "simply answer: yes or no"
]
class func getAnswer(answer: String) -> String
{
let expectedAnswer = questionItem["answer"]
let isEqual = (answer == expectedAnswer)
var respond = "undifined"
if isEqual {
// go for next question ( questionItem["afterAnswer"] )
respond = "ok, going to the next question"
} else {
// didn't got the answer I was waiting for, going to show some longer explanation
respond = questionItem["explanation"]!
}
// respond
return respond
}

You can create a struct with your desired information, then create a Trivia of Type Int: YourStruct to manage all the different questions answers and explanations, like my example below, let me know is its clear enough:
struct QuestionsAndAnswers{
var question: String = ""
var answer: String = ""
var explanation: String = ""
}
var trivia: [Int: QuestionsAndAnswers] = [:]
trivia = [1: QuestionsAndAnswers.init(question: "Your First Question", answer: "Your Fist Answer", explanation: "Your First explanation")]
trivia = [2: QuestionsAndAnswers.init(question: "Your Second Question", answer: "Your Second Answer", explanation: "Your Second explanation")]
func getAnswer(answer: String, quesiontNumber: Int) -> String
{
let expectedAnswer = trivia[quesiontNumber]?.answer
let isEqual = (answer == expectedAnswer)
var respond = "undifined"
if isEqual {
// go for next question ( questionItem["afterAnswer"] )
respond = "ok, going to the next question"
} else {
// didn't got the answer I was waiting for, going to show some longer explanation
respond = (trivia[quesiontNumber]?.explanation)!
}
// respond
return respond
}

Related

make a button display an element each time its pressed

I am new to Swift, and I am trying to make a basic question answer app. I want to set up the question button so that whenever I press it, it displays one question, and if I press it again, it shows another question. I have a separate button that will show the answer but I need to connect to which ever question is being asked. How do I do this?
Here is what I have so far, it just asks the question at random, but I want to be able to ask all the questions, not just whatever it picks, I am not sure how to do that though.
#IBAction func question(_ sender: Any) {
let questions = ["What is your name?", "What is your favourite colour?",
"What is your favourite movie?", "What is your major?"]
let randomQuestion = questions.randomElement()
qLabel.text = randomQuestion
}
You can try
var index = 0 // add a current index of the shown question
let questions = ["What is your name?", "What is your favourite colour?",
"What is your favourite movie?", "What is your major?"]
#IBAction func question(_ sender: Any) {
if index < questions.count {
qLabel.text = questions[index]
index += 1
}
else {
// end of questions
}
}

Swift Unwrapping optional class variable yields failure

In my code I have two classes: Reviews and BlogPosts:
class Review {
var author = ""
var stars = 0
}
class BlogPost {
var title = ""
var body = ""
var author = ""
var review: Review?
}
The review variable in BlogPost is optional, since not all blog posts might have a review.
I have a function which prints the amount of stars of a post:
func checkForPostStars(post: BlogPost) {
if let review = post.review {
print("\"\(post.title)\" has: \(review.stars) stars")
} else {
print("There is no review for the post.")
}
}
I then create two blog posts. The first one has no review, meaning that the function should print "There is no review for the post". For the other review I add an author and a star amount, but when I run the function it will still print "There is no review for the post".
var firstPost = BlogPost()
firstPost.title = "Famous developer has died!"
firstPost.body = "Lorem ipsum"
firstPost.author = "Riccardo Perego"
var secondPost = BlogPost()
secondPost.title = "iOS 12 is finally out!"
secondPost.body = "Lorem ipsum"
secondPost.author = "Riccardo Perego"
secondPost.review?.author = "John"
secondPost.review?.stars = 4
checkForPostStars(post: firstPost)
checkForPostStars(post: secondPost)
I have discovered that I can fix the issue by adding secondPost.review = Review() nevertheless, I would like the compiler to do it automatically as soon as it sees that I set a value for stars or author. Thanks.
The problem lies within these 2 lines:
secondPost.review?.author = "John"
secondPost.review?.stars = 4
The review is not initialized. It's like you would set some value to nil and expect it's properties to live even if it doesn't work... That's why the ? is there.
You should add constructor to the Review:
class Review {
var author: String
var stars: Int
init(author: String = "", stars: Int = 0) {
self.author = author
self.starts = stars
}
}
Also it is good practice not to assign the variables on class level scope, rather use it in initialiser.
Simply the problem is that you do not create Review instance, so you cannot add properties to it... you should handle it like this:
secondPost.review = Review(authoer: "John", stars: 4)
Also, for performance reasons, you should make the Review object struct instead of class...
So if you create a struct instead, Swift figures out the initialiser for you and life's even better:
struct Review {
var author: String
var stars: Int
}

Testing for Algorithm Method

I am relatively new to unit testing and was wondering how I can check to ensure my algorithm would correctly return any the most common word in any given string that's passed to my function?
var test = "Let's figure. out what the most comm$on wor!!!!d is in the sentence."
func checkForRandomChracters(string: String) -> String {
let charactersToRemove = Array(".:?!,##$%^*,")
let arrayOfChars = Array(string)
let filteredString = arrayOfChars.reduce("") {
let str = String($1)
return $0 + (charactersToRemove.contains($1) ? "" : str)
}
return filteredString
}
func mostFrequentWords(string: String) -> String {
let lowerString = string.lowercased()
let cleanedString = checkForRandomChracters(string: lowerString)
let array = cleanedString.components(separatedBy: " ")
var itemInDictionary: [String: Int] = [:]
for item in array {
itemInDictionary[item] = (itemInDictionary[item] ?? 0) + 1
}
var mostCommonNameInArray = ""
for key in itemInDictionary.keys {
if mostCommonNameInArray == "" {
mostCommonNameInArray = key
} else {
let count = itemInDictionary[key]!
if count > itemInDictionary[mostCommonNameInArray]! {
mostCommonNameInArray = key
}
}
}
return mostCommonNameInArray
}
mostFrequentWords(string: test) // returns 'the'
Unit test is not about ensuring that the answer will be correct for all strings. For that, you would need a formal proof, that is much more complex and related to mathematics and this kind of stuff (in a really high level description).
Unit testing is about developing tests to guarantee that your method will work for the cases you developed the tests for. And because of that, you will want to develop different cases to test different behaviours, for example:
What is the algorithm result when you use a string that you already know the result? Will It be correct?
What is the algorithm result when you send an empty string?
What is the algorithm result when there is more than one most frequent word?
Remember to always test corner cases (like empty or with more than one most frequent word), and develop a few tests for cases when you know the result, to guarantee.
Oh, and also, unit tests are really useful when you want to change the algorithm, but want to test if it's behaviour did not change.
In Swift, you can create an unit test class , then develop different tests methods with XCTest. A great tutorial you can follow to understand that is presented here.
I hope I've answered your question. Good luck.

Xcode Swift: Converting Arrayed String With If Statement

This is a simple quiz app with a label and four buttons. I want code in the action button to execute by referencing the question in the if statement. (The problem with referencing the answer tag is that there are only four buttons, but more than four questions.) The code below gives an error that you can't use the binary operator. How can I make this work?
struct Question {
var Question : String!
var Answers : [String]!
var Answer : Int!
}
var Questions = [Question]()
var QNumber = Int()
var AnswerNumber = Int()
Questions = [Question(Question: "One", Answers: ["", "", "", ""], Answer: 0),
Question(Question: "Two", Answers: ["", "", "", ""], Answer: 1),
Question(Question: "Three", Answers: ["", "", "", ""], Answer: 2),
Question(Question: "Four", Answers: ["", "", "", ""], Answer: 3),
Question(Question: "Five", Answers: ["", "", "", ""], Answer: 0),]
func PickQuestion(){
if Questions.count > 0{
QNumber = 0
QLabel.text = Questions[QNumber].Question
AnswerNumber = Questions[QNumber].Answer
for i in 0..<Buttons.count{
Buttons[i].setTitle(Questions[QNumber].Answers[i], for: UIControlState.normal)
}
Questions.remove(at: QNumber)
}
#IBAction func Btn4(_ sender: Any) {
if(Question == "Five") {
//CODE THAT NEEDS TO EXECUTVE
} else if(Question == "Four") {
//EXECUTE A DIFFERENT CODE
}
Your question is super unclear and it looks like you would benefit from re-structuring your logic. That said, if this is a simple quiz app and you have a few buttons with tags 0, 1, 2 and 3 then you should simply be able to compare the Question's Answer property with the buttons tag and avoid comparing any strings altogether.
It's also not clear from your question how the "current question" is selected, so you may want to firm that one up too, I'd recommend storing the object in a var somewhere so you can do something like the following...
var currentQuestion:Question!
// Get the first question or something.
currentQuestion = Questions.first
#IBAction func buttonTapped(sender: UIButton) {
if sender.tag == currentQuestion.Answer {
print("You are a winner")
// Time to get a new question son.
}
}
The above code is untested and I hope it doesn't just confuse you further, however, in the current format your question may be closed as it is not completely clear what you are asking.
Edit:
Thanks for updating your question, It looks now like you are trying to compare the Question struct against the String "Five". these two objects are not comparable.
To make your code work you should use the AnswerNumber variable that you have made and check if the number matches like so.
#IBAction func Btn4(_ sender: Any) {
if AnswerNumber == 4 {
print("Correct Answer")
//CODE THAT NEEDS TO EXECUTVE
} else {
print("Wrong Answer")
//EXECUTE DIFFERENT CODE
}
}
Assuming you have an IBAction for each button you will need to repeat this for each, so Btn5 would look like this.
#IBAction func Btn5(_ sender: Any) {
if AnswerNumber == 5 {
...
Edit:
After chatting away, we figured out that you needed a custom action for each question (if the correct answer was selected). this took the form of an mp3 file that was played depending on which question it was.
We came to the conclusion that following the existing structure you should add another variable to hold the mp3 in the Question struct but also for the current question as below.
struct Question {
var Question : String!
var Answers : [String]!
var Answer : Int!
var audioFile: String!
}
var AnswerMP3 = ""
Then when we set the current question alongside AnswerNumber we can set the mp3 like so.
AnswerMP3 = Questions[QNumber].audioFile
Then in this way you do not need to have hardcoded actions for each question. the buttons simply pass the correct mp3 on to another method that plays the audio file.
if AnswerNumer == 4 {
playMP3File(AnswerMP3)
}
func playMP3File(fileName:String) {
// user the fileName to play the audio file.
}

How do I 'seed' using GKRandomSource from Swift's Gameplaykit to remember the shuffle between sessions

I'm new to programming and have been learning Swift by doing a number of online courses. In one of the courses we built a basic trivia game and I've been progressively trying to improve it by doing my own coding (best way to learn!).
Recently I came across what's called a Fisher-Yates shuffle and, after much trial and error (and with the help of the stack overflow community) was able to use GKRandomSource from Swift's Gameplaykit to shuffle my trivia questions around so that they were being asked randomly. This was an improvement on the original arc4random code I was using because the shuffle removed questions already asked from the overall pool of questions, thereby ensuring that they did not repeat (at least in iOS9).
This works well within the session, but once the user quits the app and relaunches it, the shuffle starts from scratch. So I was looking at a way to have the app 'remember' the questions already asked between sessions. My research led me to the idea of seeding and I've been trying to get this to work with my GKRandomSource code, but I'm obviously missing something.
Any advice etc would be most welcome - especially since I'm not entirely sure that this 'seeding' approach will achieve my ultimate aim of not repeating questions already asked in previous sessions of the app.
Below are what I believe to be the relevant bits of my revised code.
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 my most recent code for trying to achieve the shuffle of all questions and replicate it in future sessions):
var allEntries : NSArray!
var shuffledQuestions: [AnyObject]!
var nextQuestion = -1
var mySeededQuestions : [AnyObject]
loadAllQuestionsAndAnswers()
if #available(iOS 9.0, *) {
let lcg = GKLinearCongruentialRandomSource(seed: mySeededQuestions)
let shuffledQuestions = lcg.arrayByShufflingObjectsInArray(allEntries)
nextQuestion++
loadQuestion(nextQuestion)
// Fallback on earlier versions
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know at the very least I have a problem with the above code, but I'm at a loss. I'm also thinking that maybe I'm missing a step in terms of storing the seed?
For the sake of completeness, 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 = shuffledQuestions[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()
}
func loadQuestionPreiOS9(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, *) {
nextQuestion++
loadQuestion(nextQuestion)
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know my coding is probably quite verbose and unnecessary, but up until this latest improvement it's been working fine and I actually understand most of it (I think!)
There are really two questions here: what you're asking about and what you seem to want. They're both worth answering for different reasons, so...
How to seed a GK(Whatever)RandomSource
(All the GKRandomSource subclasses have seeds, even though the superclass GKRandomSource itself doesn't... that's because each class has its own data type for seeds. But the usage is the same.)
The critical bits of the code you've posted don't even compile due to a type mismatch: the seed/init(seed:) value for GKLinearCongruentialRandomSource is an integer, not an array of objects. The documentation for that value spells out what it's for (emphasis added) and how to use it:
Any two random sources initialized with the same seed data will generate the same sequence of random numbers. To replicate the behavior of an existing GKLinearCongruentialRandomSource instance, read that instance’s seed property and then create a new instance by passing the resulting data to the initWithSeed: initializer.
So, if you want to replicate a sequence of random numbers:
Create a random source with the plain initializer.
let source = GKLinearCongruentialRandomSource()
Save off that source's seed value.
let seed = source.seed // -> some UInt64 value
// write seed to user defaults, a file, a web service, whatever.
Use that random source for whatever.
Later, when you launch again and want the same sequence, read in the seed value and create a random source using the seed.
let seed = // read in seed value from wherever you saved it
let source = GKLinearCongruentialRandomSource(seed: seed)
This still doesn't get you what you're actually looking for, though: If source in step 1 produced the sequence 1, 6, 3, 9, 2, 7, source from step 4 will also produce the sequence 1, 6, 3, 9, 2, 7 — the seed doesn't record where you "left off" in a sequence. Or, since you're using it for an array shuffle, it'll produce the same shuffled ordering of the array as the first shuffle, but it doesn't remember what you did with the shuffled array thereafter.
How to use a shuffled ordering across multiple app launches
If you want to shuffle an array, walk through it in order, and then on a later run of your app continue walking through the same shuffled array from where you left off, you need to design around that requirement.
Shuffle on the first launch.
Record something about the ordering produced. (Say, a mapping of indices in the shuffle to indices in the original data.)
When walking through the shuffled array, record how far you've gone through it.
On later runs of the app, use the record of the ordering and the record of progress to decide where you are.
Here's a rough pass at that. (Note that I'm not touching your data model — this is a program design question, and SO is not a coding service. You'll need to think about how to flesh out this design to match your model and its use cases.)
struct Defaults {
static let lastQuestionIndex = "lastQuestionIndex"
static let questionOrder = "questionOrder"
}
let questions: [Question] // array of model objects, always in fixed order
func nextQuestion() -> Question {
let defaults = NSUserDefaults.standardUserDefaults()
if let lastIndex = defaults.integerForKey(Defaults.lastQuestionIndex) {
// we've run before, load the ordering
guard let shuffledOrder = defaults.arrayForKey(Defaults.questionOrder) as? [Int]
else { fatalError("save questionOrder with lastQuestionIndex") }
// advance the saved index so the next call to this function
// will get the next question
if lastIndex + 1 < count {
defaults.setInteger(lastIndex + 1, forKey: Defaults.lastQuestionIndex)
} else {
// ran out of shuffled questions, forget the order so we
// can reshuffle on the next call
defaults.removeObjectForKey(Defaults.questionOrder)
defaults.removeObjectForKey(Defaults.lastQuestionIndex)
}
// map lastQuestionIndex from sequential to shuffled
// and return the corresponding answer
let shuffledIndex = shuffledOrder[lastIndex]
return questions[shuffledIndex]
} else {
// first run, shuffle the question ordering (not the actual questions)
let source = GKRandomSource()
let sequentialOrder = Array(0..<questions.count)
let shuffledOrder = source.arrayByShufflingObjectsInArray(sequentialOrder)
// save the ordering, and the fact that we're asking the first question
defaults.setObject(shuffledOrder, forKey: Defaults.questionOrder)
defaults.setInteger(0, forKey: Defaults.lastQuestionIndex)
// return the first question in the shuffled ordering
let shuffledIndex = shuffledOrder[0]
return questions[shuffledIndex]
}
}
That's probably a bit pseudocode-ish (so you might need to worry about casting arrays to work with NSUserDefaults, etc), but as a general design it should be enough to give you some food for thought.
You can also use the following to drop off a certain amount of values so if you keep a roll count dropping that many on next start is as easy as :
arc4.dropValues(rollCount)