iOS Swift – Building a Times Tables App – Part 4

Today is a good day. We’re going to finish the iOS times tables practice app we started a while ago. The first three parts are here:

iOS Times Tables App – Part 1
iOS Times Tables App – Part 2
iOS Times Tables App – Part 3

The full code for this app is on my Github page:

https://github.com/jamesmcclay/swift_times_tables

Today we will be adding the actual times tables flashcard functionality to the app.

Add the UI elements

As it turns out, you don’t really need to use the init functions I used in part 1. Oh well, live and learn. You can just add them at the top of your class as global variables like this:

//Displays the question
let questionLabel:UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = ""
    label.numberOfLines = 2
    label.textAlignment = .center
    label.font = UIFont.preferredFont(forTextStyle: .title1)
    label.adjustsFontForContentSizeCategory = true
    return label
}()
//Displays the answwer
let answerLabel:UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = ""
    label.numberOfLines = 2
    label.textAlignment = .center
    label.font = UIFont.preferredFont(forTextStyle: .title2)
    label.adjustsFontForContentSizeCategory = true
    return label
}()
//Either shows the answer or goes to next question
let button:UIButton = {
    let button = UIButton(type: .system) as UIButton
    button.translatesAutoresizingMaskIntoConstraints = false
    button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title2)
    button.titleLabel?.adjustsFontForContentSizeCategory = true
    button.setTitle("Show Answer", for: .normal)
    return button
}()

//These are variables we'll need to keep track of the app state
var mode:String = "normal"
var table:Int = 0
var multiplier:Int = 0
var selectedTable = 0
var maxMultiplier:Int = 0
var lastMultiplier = 0
var answerShowing = false

We add three UI elements – questionLabel, answerLabel, and button. Each one does what it sounds like, and button will either show the answer or move on to the next question. Here’s the non-UI variables and what they will do:

  • mode is either “normal” or “random”. Normal starts the multiplier at 2 and goes to until maxMultiplier, then goes back to 2. Random picks a random multiplier between 2 and maxMultiplier.
  • table holds the current table being practiced. In all tables mode, it will be incremented up to 9, then go back to 2.
  • multiplier is the second number in a multiplication question.
  • selectedTable holds the table the user originally selected. If it’s anything other than 0 (the user didn’t select a table), selectedTable will be the same as table since the table won’t change. If selectedTable is 0, table will be incremented or changed randomly, depending on the mode. selectedTable needs to stay the same so that the app knows whether a table was selected or to practice them all.
  • maxMultiplier indicates how far up the user wants to practice in the table or tables. Defaults to 12.
  • lastMultiplier holds the value of the last multiplier. This exists to solve a problem where the same multiplier is selected multiple times in a row in random mode. Using a while loop, we can check if the newly selected multiplier is different from the last one. No need to practice the same question twice in a row.
  • answerShowing tells the app whether the answer is showing or not. Lets us control what code to run when the button is pressed, and what text to put on the labels.

These all have dummy values, because their actual values will be set in viewWillAppear. The UINavigationController reuses old UIViewControllers, so this code only gets run once. viewWillAppear runs every time the view shows, so we’ll set values there.

Layout the UI elements

Like last time, we’ll run several functions to create autolayout constraints for the UI elements, to put them in their proper spots. The viewDidLoad function looks like this:

override func viewDidLoad() {
    super.viewDidLoad()
    setupQuestion()
    setupAnswer()
    setupButton()
}

Now we’ll create each of those functions:

func setupQuestion() {
    view.addSubview(questionLabel)
    questionLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -(view.frame.height * 0.1)).isActive = true
    questionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}

func setupAnswer() {
    view.addSubview(answerLabel)
    answerLabel.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: view.frame.height * 0.1).isActive = true
    answerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    
}

func setupButton() {
    view.addSubview(button)
    button.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: view.frame.height * 0.2).isActive = true
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
}

Basically everything here is attached to the questionLabel, which we have constrained to 10% above the center Y of the view per -(view.frame.height * 0.1). answerLabel is 10% below that, while button is 20% below. The last line of the button function adds a function called buttonPressed as the function to run when the button is pressed.

Add the logic

As I mentioned earlier, we need to set the global variables’ values in viewDidAppear. It looks like this:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(true)

    //Show the nav bar
    self.navigationController!.navigationBar.isHidden = false

    //give the global variables some default values.
    table = 2
    multiplier = 1
    selectedTable = 0
    maxMultiplier = 12
    lastMultiplier = 0
    answerShowing = false

    //Get the user's specified table, if it exists. 0 means no selected table
    if let userTable = UserDefaults.standard.value(forKey: "table") as? Int {
        if userTable != 0 {
            table = userTable
        }
        self.selectedTable = userTable
    }
    //Get the user's specified max multiplier, if it exists. 0 means no selected multiplier
    if let multi = UserDefaults.standard.value(forKey: "multiplier") as? Int {
        if multi != 0 {
            maxMultiplier = multi
        }
    }
    //Get the mode
    if let practiceMode = UserDefaults.standard.value(forKey: "mode") as? String {
        mode = practiceMode
    }
    //Run nextQuestion once at the start.
    nextQuestion()
}

I added comments inline for this one. Please let me know in the comments if anything is not clear.

At this point, all of our values are set. We need to write out nextQuestion, as it is called in the last line. nextQuestion looks like this:

func nextQuestion() {
    answerLabel.text = "" //Clear the answer
    if mode == "normal" {
        multiplier += 1 //Increment the multiplier every time
        if multiplier > maxMultiplier { 
            multiplier = 2  //Go back to 2 if at max
            if selectedTable == 0 {
                table += 1 //Increment table if in all tables mode and current table is finished (multiplier at max)
                if  table == 10 {
                    table = 2 //Go back to table 2 if all tables finished.
                }
            }
        }
    } else {
        //This is random mode
        lastMultiplier = multiplier
        if selectedTable == 0 {
            table = Int.random(in: 2...9) //If no selected table, get a random one from all tables
        }
        //Get a new multiplier until it's different from the last one, avoids duplicate question.
        while multiplier == lastMultiplier {
            lastMultiplier = multiplier
            multiplier = Int.random(in: 2...maxMultiplier)
        }
    }
    questionLabel.text = "\(table) x \(multiplier) = " //Display question.
    answerShowing = false  //Answer no longer showing
    button.setTitle("Show Answer", for: .normal) //Change button text to "Show Answer"
}

I added comments inline for this one too. The logic here should take care of all use cases.

Now we’ll write the function to show the answer:

func showAnswer() {
    answerLabel.text = "\(table * multiplier)"
    answerShowing = true
    button.setTitle("Next Question", for: .normal)  
}

This one is short and sweet. Just show the answer, set answerShowing to true, and set the button text to “Next Question”.

Finally, the code that runs when the button is pressed:

@objc func buttonPressed() {
    if answerShowing {
        nextQuestion()
    }else{
        showAnswer()
    }
}

If the answer is showing, run nextQuestion. If not, run showAnswer.

We’re done!

Try it out!

Things are looking good! Be sure and try all modes and variables you can think of 🙂

iOS Simulator in Xcode

Leave a Reply

Your email address will not be published.