UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 35 - multiplication challenge problems

Forums > 100 Days of SwiftUI

@CJ  

Hi

I wonder if anyone can help with this.

I have been attempting the timestable game challenge from day 35 and although I was able to come up with a question generator based on a user input, I am having trouble making it recognise the answer correctly above the 2 times table. I should say I did not attempt to create a new view as I felt it enough of a challenge as it was and did not want to overload myself with problems. Instead I just added a textfield for the user to input their answer. I am wondering, however, if this is part of the problem as the warning message I am getting in XCode is 'Modifying State during view update, this will cause undefined behaviour' (all occurring in the 'multiplication2' function below). Unfortunately, I built it initially on the ipad and got none of these errors (although it worked unpredictably, it didn't show any warnings). I really have no idea how to go about creating a different view and am not sure if there is a way round this. Any advice would be welcome.

As an aside, after experiencing these issues, I looked at Paul's hints and tried to approach it in the suggested way, building a question struct and creating a question array but again reached a dead end trying to transfer the array builder function (which all worked fine in playgrounds) from the struct and making it work in the main view struct on the app (keep getting idex of range errors) so have now reached dead ends from two different approaches which is all very frustrating.

I've included the main bits of the code from my first approach below. I also made a question generator and various alerts that I have attached to a button that the user presses to get the next question (I can add these if helpful). As I say, it works as expected up to 2 times table but above that stops recognising the answers as correct and so doesn't score properly.

@State private var showingSum = false
  @State private var timesTable = 0
  @State private var questionSelect = false
  @State private var questionCounter = 0
  @State private var anotherRandomNumber = Int.random(in: 2...12)
  @State private var numberArray = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].shuffled()
  @State private var gameOver = false
  @State private var numberOfQuestions = 0
  @State private var animationAmount = 0.0
  @State private var userAnswer = 0
  @State private var score = 0
  @State private var startTest = false
  @State private var levelArray = [Int]()
  @State private var showingStart = true

  //make difficulty equal user selection
  var difficulty: Int {
    return timesTable + 2
  }

  //create question and check answer
  func multiplication2 () -> String  {

    //select a number from 1 - 12 from an array
    let firstNumber = numberArray [0]
    //generate a random number within user specified range

        anotherRandomNumber = Int.random(in: 2...difficulty)

    let answer = anotherRandomNumber * numberArray [0]
    questionCounter += 1
    //check user answer against correct answer
    if answer == userAnswer {
      score += 1
      userAnswer = 0

    }else {
      score = score
    }
    return ("\(firstNumber) x \(anotherRandomNumber) = \(answer)")
  }

This is the textfield:

    TextField("answer", value: $userAnswer, formatter: formatter)
            .keyboardType(.numberPad)

this is the timestable difficulty level picker

   Picker("", selection: $timesTable) {
            ForEach(2..<13) {
              Text("up to the \($0) timestable")
            }

2      

Well... your multiplication2() function is pretty wild. You are expecting it to generate a question and determine if the answer is correct or not all at the same time. This means that when this function is called it will create a question and then immediate check if the user has entered the correct answer before it even shows them the question.

You will probably need to take Paul's advice and create a separate function generateQuestions() that will check the difficulty setting that the user has selected, and generate all of the possible questions, along with their answers, in a Dictionary. This is going to need to be called after the user has entered their settings, and is ready to start the game. So, if you aren't going to create separate views for your different game states, then you will need a button to start the game.

Then, you will want a separate function called askQuestion(), which will select a random entry from the dictionary, and display the question to the user. This will need to be called after the questions are generated, of course.

Then, you will need another separate function checkAnswer() that should handle checking the answer, updating the score, and things like that before asking the next question (by calling the askQuestion() function again.) The checkAnswer() function will need to be called when the user taps a button to submit their answer, so that it doesn't try to check the answer before they have finished typing it in.

So, I would start by adding this property to hold all of your questions as String and answers as Int

@State private var questions: [String : Int] = [:]

Then work on creating this function...

func generateQuestions() {
    //Generate all possible questions based on the selected difficulty and add them to the questions Dictionary
}

2      

@CJ  

Many thanks for your response @Fly0strich . The function is indeed 'wild.' The problem I have been having is making certain variables accessible across different methods. The @State wrappers and computed properties etc. can work to a point but then sometimes I reach a dead end and that's why things get shoehorned into the same function. I'm guessing this could be where structs come in useful. I didn't think to use them initially as my brain is still to some extent compartmentalising the playground concepts and practical swiftUI. I tend to draw on ideas from apps we have already made which can work to an extent but not always.

Anyway, thanks for the advice. I hadn't thought at all about dictionaries as I can hardly remember doing them but, having looked through my notes, I'm guessing you mention them as they allow you to store strings and integers together but as discrete values. I created the struct below in a playground mainly to see if I could make a timestable generator based on difficulty level. Anyway it works and I created arrays easily enough, I couldn't then work out how to create a dictionary from the loop. The next thing, of course, will be how to then transfer this to the @State property you mention (or should I make the function within the main view (can I do that?)?). I did wonder if I could use the arrays by somehow mapping the corresponding indexes to one another but I was geting string/integer conflict errors, so that didn't seem viable.

Thanks again for your help and apologies for my probably quite basic questions!

struct Question  {
    var difficultyLevel : Int
    var twoToTwelve = 2...12
    var answer = 0
    var questionArray = [String]()
    var answerArray = [Int]()
    var dictionary = [String: Int]()
    mutating func multiply ()  {
        //create all the timestables within user specified range and make them into an array
        for i in twoToTwelve  {
            for j in (2...difficultyLevel) {
                answer = i * j
                questionArray.append ("\(i) x \(j) = ? ")
                answerArray.append(answer)
                print("\(i) x \(j) = \(answer)")
            }
            }
    }

}

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

I would rename your Dictionary to something like questions instead. But, you can add items to a dictionary by using the updateValue function. Then, you won't need your two separate arrays anymore.

dictionary.updateValue(answer, forKey: "\(i) x \(j) = ? ")

Any variables that you declare will basically be accessible to anything within the same set of curly braces that they are declared in, but nowhere outside of it.

For example...

struct ContentView: View {
    @State private var myNumber = 0 
    // Here, we are declaring a variable within the curly braces of the ContentView struct

    var body: some View {
        Text("\(myNumber)")
        // We can still access the variable here, because we are still within the closing curly brace of the ContentView struct
        // even though we are inside of another set of curly braces of the body property
    }

    func addOne() {
        var randomGreeting = "hi"
        // Here, we are declaring a variable within the curly braces of the addOne function

        myNumber += 1
         // We can still access the myNumber variable here, because we are still within the closing curly brace of the ContentView struct
    }

    func doNothing() {
        print(randomGreeting)
        // This will not work because randomGreeting was declared inside the curly braces of the addOne function.
        // We are no longer inside of those curly braces here.

        print("My favorite number is \(myNumber)
        // But this will work, because myNumber was declared inside of ContentView directly, and we are still
        // within the curly braces of ContentView
    }
}

struct SomeOtherView: View {
    var body: some View {
        Text("\(myNumber)")
        // But this won't work.  We are outside of the braces of ContentView now.
        // Since myNumber was created inside of the braces of ContentView it only exists within the braces of ContentView
    }
}

2      

@CJ  

Thanks again for your advice and explanation.

My main issue with variables has been when declaring them in relation to another variable. 'answer' in the function I first posted for instance. If I declare it in the main view it will throw up an error message about property initilaisers not running before 'self is available.' If I put it in the question function, however it will be available but only to the question function and unless I can get it to return 2 values then I have no way to access a value for an answer checking function. I can see with a dictionary you get two values at the same time so perhaps that does solve the problem. Now I'm not sure whether I should just try to generate a dictionary from a function within the content view or use the question struct. I actually did just try the former within a playground but it gave error messages (String.Type cannot conform to hashable - no idea what that means but I could have just gone about it in the wrong way). Anyway, I'll have another play around and see if I can get anywhere with either approach....

Thanks again

2      

If you try to do it this way, you will run into those errors.

struct ContentView: View {
    @State private var factorOne = Int.random(in: 0...12)
    @State private var factorTwo = Int.random(in: 0...12)
    @State private var answer = factorOne * factorTwo

    var body: some View {
        VStack {
            Text("\(factorOne) x \(factorTwo)")
            Text("\(answer)")

            Button("Next Question") {
                askQuestion()
            }
        }
    }

    func askQuestion() {
        factorOne = Int.random(in: 0...12)
        factorTwo = Int.random(in: 0...12)
    }
}

This is because when you create an instance of any struct (like ContentView), it will expect that all of its properties are set with initial values. However, it does not set the values of each property in any specific order. So, it can't be sure that all of the properties have been set with their initial values until after the creation of the instance has finished. So, this error is basically telling you, "I can't set the initial value of answer by using values that I'm not sure have been set yet. I can't be sure that factorOne and factorTwo have been set with values yet."

One way to get around this is by using a computed property.

struct ContentView: View {
    @State private var factorOne = Int.random(in: 0...12)
    @State private var factorTwo = Int.random(in: 0...12)

    var answer: Int {
        factorOne * factorTwo
    }

    var body: some View {
        VStack {
            Text("\(factorOne) x \(factorTwo)")
            Text("\(answer)")

            Button("Next Question") {
                askQuestion()
            }
        }
    }

    func askQuestion() {
        factorOne = Int.random(in: 0...12)
        factorTwo = Int.random(in: 0...12)
    }
}

This way, answer does not have to have an initial value in order for an instance of ContentView to be created. So, it will make sure that factorOne and factorTwo are initialized with values, but it will just know that the value of answer can be calculated later when it is needed.

Another option is to just set answer to a default value that doesn't involve using the other variables to begin with

struct ContentView: View {
    @State private var factorOne = 0
    @State private var factorTwo = 0
    @State private var answer = 0

    var body: some View {
        VStack {
            Text("\(factorOne) x \(factorTwo)")
            Text("\(answer)")

            Button("Next Question") {
                askQuestion()
            }
        }
        .onAppear(perform: askQuestion)
    }

    func askQuestion() {
        factorOne = Int.random(in: 0...12)
        factorTwo = Int.random(in: 0...12)
        answer = factorOne * factorTwo
    }
}

In this example, we are just setting all of our properties to zero to begin with, and then using our askQuestion() function to set all of our values. This method uses onAppear(perform: askQuestion) to make sure that a question is loaded as soon as our view loads for the first time. Otherwise, when the program first starts, it would be showing "0 x 0 = 0" and we might not want that. But if you are fine with that, then it is not required.

One final way that you should be aware of to get around having to initialize all of your properties is using optionals.

struct ContentView: View {
    @State private var factorOne = 0
    @State private var factorTwo = 0
    @State private var answer: Int?

    var body: some View {
        VStack {
            Text("\(factorOne) x \(factorTwo)")
            Text("\(answer ?? 0)")

            Button("Next Question") {
                askQuestion()
            }
        }
        .onAppear(perform: askQuestion)
    }

    func askQuestion() {
        factorOne = Int.random(in: 0...12)
        factorTwo = Int.random(in: 0...12)
        answer = factorOne * factorTwo
    }
}

This is very similar to our last example, but you can see that answer is declared as an optional Int? here and we aren't giving it any initial value at all. In this case, when an instance of ContentView is created, it will automatically set the initial value of answer to nil. But, while using this method, you will see that we had to change this line a bit, to account for the possibility that answer is nil

Text("\(answer ?? 0)")

That just makes sure that a zero would show in the text view in that case.

2      

@CJ  

Thanks again for all this. It is good to have a deeper understanding of what is going on. With regard to computed properties, I think I did use them initially but for some reason the answer to the question was not syncing with the question, so I think I then abandoned the idea. Part of my problem is if things don't work, I don't always know why and so possibly move on prematurely.

Anyway, I do have another question(sorry)

I am struggling to get the dictonary to return a value from the key - it is strange because it works in playgrounds but not in SWiftUI. In Playgrounds, it didn't work initially but then I adjusted the spacing and it was fine. This didn't work in SWiftUI however. I tried using both a nil coalescing operator and a default value but neither worked and just returned 0. The function is using values from the Question struct. It is not at all organised at this stage as I am just trying to make sure that values are pulling through. I am just unclear why it is not returning a value.

func generateQuestionNew () {
            //set difficulty level for questions
              var question = Question(difficultyLevel: difficultyLevel )
              //create dictionary based on user input
             let questionsNew = question.questions
              question.multiply()
              print(question)
              print(questionsNew["4 x 2 = ?"] ?? 0)
              print(questionsNew["4 x 2 = ?", default: 0])
          }

This is the console print out:

Question(difficultyLevel: 2, twoToTwelve: ClosedRange(2...12), answer: 24, questions: ["11 x 2 = ?": 22, "5 x 2 = ?": 10, "8 x 2 = ?": 16, "2 x 2 = ?": 4, "12 x 2 = ?": 24, "3 x 2 = ?": 6, "4 x 2 = ?": 8, "6 x 2 = ?": 12, "9 x 2 = ?": 18, "7 x 2 = ?": 14, "10 x 2 = ?": 20]) 0 0 ()

2      

@CJ  

update - dictionary does return values when created in a function within contentView just not from the struct mutating function - so maybe I'll do that!

2      

It's hard to say what the problem that you are running into is coming from in that case, without seeing more of your code. But for one, I'm not exactly sure what your Question struct is needed for, or what exactly it is being used for here. In my project, I didn't use a Question struct at all. I just created the dictionary [String: Int] and that was all the data I really needed for my questions.

This is declared in the scope of my custom View in my project, but you could declare it in ContentView if that is where your generateQuestions() function is declared.

@State private var questions: [String : Int] = [:]

I'll show you how I created my generateQuestions() function...

//This function fills our questions dictionary with all possible questions, based on the selected difficulty.
func generateQuestions() {
        //We loop through all numbers 0-12
        for factorOne in 0...12 {
            //For each number 0-12, we create questions multiplying that number by each number 0-difficulty
            for factorTwo in 0...difficulty {
                //And we add each question to our dictionary
                questions.updateValue(factorOne * factorTwo, forKey: "\(factorOne) x \(factorTwo) = ")
            }
        }
        //Once the dictionary is filled with all possible questions, we ask a random question from the dictionary and present it to the user
        askQuestion()
}

But in my project, I allowed all values from 0-12. If you want to only allow numbers > 1 you could easily modify those ranges.

Then, the askQuestion() function basically just uses questions.randomElement() to select a random question from the dictionary. But, the randomElement() function does return an optional value, because it might return nil if the dictionary is empty when you call it. So you do have to make sure you are unwrapping the optional in some way.

You'll often see Paul suggest this along the way of this course, but one thing to keep in mind when trying to create a project is to think small at first. Don't try to build your whole project at once. First just think, "What is the minimum requirement that I need to meet to make this project functional?" In this case, it is basically just "I need to generate all possible questions based on the selected difficulty, and put them into a dictionary with questions of type String, and answers of type Int. Then, I need to be able to display a random one of those questions on the screen." So, you should aim to do that in the simplest way possible. Once you get that working, you can add more features as needed (Like, giving the user some way to answer a question that is presented to them, then making sure that their answer is checked for correctness.)

But so far, we have no real need to create a separate struct for our questions, because the dictionary is capable of holding all of the variables that we need to use for them in one place.

3      

@CJ  

Thank you again for your help. I used the struct because Paul suggests it in the hints section of the problem and I was not getting too far with my other approach. That was the only reason, bnut possibly just over compicated it for me.

Regarding the functions mentioned, I did set up this questionGenerator yesterday and it worked but then when I go to enter text in the textfield, it starts to change the factors in the question field. Looking at it in XCode I am getting errors again relating to the 'view '. I thought perhaps it is because the function is in the question generator but when I put the same as a separate askQuestion function it doesn't seem to return anything. I made an answer checker separately but I can't check it because I can't generate a question. With an array I just use array.shuffle() and it makes it send out a new value. I don't know if the dictionary needs something like this to send out a new value. It is mightily frustrating to keep hitting these brick walls I must say.

func questionGenerator ()-> String  {

        for i in twoToTwelve  {
            for j in (2...difficultyLevel) {

                //let answer = i * j

                questionsStore.updateValue(i * j, forKey: "\(i) x \(j) = ? ")

            }

        }

        if let newQuestion = questionsStore.randomElement()?.key{

            return "\(newQuestion)"

        }
        return ""
        }
    func askQuestion () -> String {
      if let newQuestion = questionsStore.randomElement()?.key{

        return "\(newQuestion)"

      }
      return ""
    }
 ```
  func answerChecker () -> Bool {
    if userAnswer == i * j {
        score += 1
        return true
    }
    return false
}
```

```
TextField("answer", value: $userAnswer, formatter: formatter)
                        .keyboardType(.numberPad)
                    Button("submit answer?"){
                        //answer checker function here
                    }

2      

I'm sorry, I've probably just been making things more confusing for you then with using the dictionary. I just looked at the project instructions, and it does say to use a Question struct, and then add each Question to an array there. I'm not sure now if the instructions have changed since I made this project, or if I just chose to use a dictionary on my own.

But, you should be able to either use a Question struct, which has a String property for the question and an Int property for the answer, and store each Question in an array. Or you could use a dictionary like I did, which will also hold a String and an Int. But, there is no need to have both. Sorry for the confusion.

If you want to use the Question struct as Paul suggests, it should look something like this...

struct Question {
    let question: String
    let answer: Int
}

Then you would want to declare this property in whichever View struct your generateQuestions() function is in

@State private var questions: [Question] = []

Then, you could modify the function I provided above to this, to get basically the same result.

//This function fills our questions array with all possible questions, based on the selected difficulty.
func generateQuestions() {
        //We loop through all numbers 0-12
        for factorOne in 0...12 {
            //For each number 0-12, we create questions multiplying that number by each number 0-difficulty
            for factorTwo in 0...difficulty {
                //And we add each question to our array
                questions.append(Question(question: "\(factorOne) x \(factorTwo) = ", answer: factorOne * factorTwo))
            }
        }
        //Once the array is filled with all possible questions, we ask a random question from the array and present it to the user
        askQuestion()
}

As you can see from comparing this to my last response, the code won't change much either way you choose to do it.

3      

@CJ  

Thanks again. Unfortunately I find this is adding both the question and the answer to the array. When I go to print(questions[0]) it prints out the whole struct. I can't just therefore pull out the question. It gives an error if I try to put ("(questions[0]) in a text box, so I think I am going to have to have another think. I will have a look at some of the other posts and try and put something together. I don't really want to take up any more of your time with it(or too much of my own!).

2      

Yes, questions[0] would refer to the whole Question struct instance. But you can use questions[0].question or questions[0].answer to get the individual properties.

2      

@CJ  

Oh my God, thank you, thank you! It is finally working - at last! I am so grateful.

3      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.