iOS Swift – Building a Times Tables App – Part 1

Previously, we built an Android times tables practice app in Kotlin, as well as a backend-only web times tables app in Python. Today we’re going to port this app to Swift so it can work on iOS for an iPhone as well. Let’s get started! You can find parts 2 and 3 of this series here:

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

At the time of writing, this was done in Xcode 12.5.1 and Swift 5.4.2. All the code for this app can be found on my github account:

https://github.com/jamesmcclay/swift_times_tables

Create a project in Xcode

We’re going to create an iOS app in Xcode, so make sure you select “iOS” in the Xcode new project window:

Xcode New Project Window

There are, in fact, many different ways to write an app in Xcode/iOS/swift. As a matter of complete personal preference, I like doing everything “programmatically”. In iOS development you have two choices: use Interface Builder or programmatically. Interface Builder uses the story board to create UI objects/elements, while programmatic creates the same elements from swift code without using Interface Builder. I happen to prefer the latter.

The only thing that is really difficult to create from code is a UIViewController, so I typically add those from the storyboard. Since Apple gives us the first UIViewController, we’ll jump right into creating the UI elements.

Create the UI objects

We’ll first create global variables to hold our UI elements so we can access them from anywhere in the class, like this:

let mainLabel:UILabel
let subLabel:UILabel
let tableInput:UITextField
let multiplierInput:UITextField
let normalMode: UISwitch
let normalLabel: UILabel
let randomMode: UISwitch
let randomLabel: UILabel
let startButton: UIButton
var modes: [UISwitch] = []

I’ll explain what each one does:

  • mainLabel: a title-like piece of text to show the name of the app.
  • subLabel: some text that tells the user to enter a table or leave it blank to practice all tables.
  • tableInput lets the user select a specific table to practice.
  • multiplierInput lets the user specify how far up they want to practice. The default is to practice to 12, but this input allows it to be configured.
  • normalMode: a switch for “normal mode” which practices in sequence.
  • normalLabel: a label attached to the switch saying “normal mode”.
  • randomMode: a switch for “random mode” which practices randomly.
  • randomLabel: a label attached to the switch saying “random mode”.
  • startButton: the button to begin practicing.
  • modes: an array to hold the two switches, more on that in the next post.

Now that we have these created, we’ll add a couple of functions that allow us to create objects to put in these variables, without ever touching Interface Builder. They look like this:

convenience init() {
    self.init()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

I won’t go into how these work, as it’s a bit distracting from the app building. Suffice to say they let us create UI objects programmatically.

Our objects can be created in the required init? function before the line with super.init is run. The way I typically create objects is using closures, which is just a function without a name. You can assign whatever the closure returns to a variable. So I just create a bunch of closures that create the objects and assign them to the global variables. This next bit is long, but it’s just a bunch of boilerplate creating these elements/objects the way that I wanted them. The required init? function now looks like this:

required init?(coder aDecoder: NSCoder) {
    mainLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "James McClay's Times Tables!"
        label.numberOfLines = 2
        label.textAlignment = .center
        label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    subLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Enter a table to practice,\n or blank for all:"
        label.numberOfLines = 2
        label.textAlignment = .center
        label.font = UIFont.preferredFont(forTextStyle: .title2)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    tableInput = {
        let input = UITextField()
        input.translatesAutoresizingMaskIntoConstraints = false
        input.placeholder = "All Tables"
        input.borderStyle = .bezel
        input.keyboardType = .numberPad
        return input
    }()
    multiplierInput = {
        let input = UITextField()
        input.translatesAutoresizingMaskIntoConstraints = false
        input.placeholder = "Max Multiplier (12)"
        input.borderStyle = .bezel
        input.keyboardType = .numberPad
        return input
    }()
    normalMode = {
        let mode = UISwitch()
        mode.translatesAutoresizingMaskIntoConstraints = false
        mode.tag = 0
        return mode
    }()
    normalLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Normal Mode"
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    randomMode = {
        let mode = UISwitch()
        mode.translatesAutoresizingMaskIntoConstraints = false
        mode.tag = 1
        return mode
    }()
    randomLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Random Mode"
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    startButton = {
        let button = UIButton(type: .system) as UIButton
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title2)
        button.titleLabel?.adjustsFontForContentSizeCategory = true
        button.setTitle("Start!", for: .normal)
        return button
    }()
    super.init(coder: aDecoder)
    modes = [normalMode, randomMode]
}

modes = [normalMode, randomMode] is put after super.init because that’s where self is created. I’ll use that array to make sure only “normal” or “random” mode can be selected, both.

Now that we have all those elements created, let’s lay them out on the view.

Using autolayout to put objects on the view

All of the elements created above have translatesAutoresizingMaskIntoConstraints set to false, which means that Autolayout is turned on. Autolayout basically allows a swift developer to put UI elements on the view and have their position dynamically calculated based on the view size and their relative position to other elements.

The way I like to do this is to write a function to hold the code for each logical area, and call each function from viewDidLoad. So my viewDidLoad looks like this:

override func viewDidLoad() {
    super.viewDidLoad()
    self.navigationController!.navigationBar.isHidden = true
    setupLabels()
    setupInputs()
    setupModes()
    setupStartButton()
}

First we’ll set up the “labels”, the main one and the sub one. My setupLabels function looks like this:

func setupLabels() {
    view.addSubview(mainLabel)
    view.addSubview(subLabel)

    mainLabel.topAnchor.constraint(
        equalTo: view.safeAreaLayoutGuide.topAnchor,
        constant: view.frame.height * 0.1
    ).isActive = true
    mainLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    mainLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8).isActive = true

    subLabel.topAnchor.constraint(
        equalTo: mainLabel.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    subLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    subLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7).isActive = true
}

I really hate how complicated this looks, because it’s not complicated. The autolayout code here is doing just a few basic things:

  • Add the UI element to the view as a subview (it puts the element on the screen).
  • Anchor its y-axis, usually by putting a constraint on the topAnchor. I usually give a constant with that one as well to provide some dynamically-created spacing, with view.frame.height * 0.1.
  • Anchor its x-axis, a lot of times it can just be constrained to the view.centerAnchor.
  • Anchor the width, usually to a percentage of the main view.

Once those constraints are in place, the element will properly show on the screen. That’s really all that’s happening here. If you have any questions, please comment or contact me!

Now let’s create setupInputs:

func setupInputs() {
    view.addSubview(tableInput)
    view.addSubview(multiplierInput)

    tableInput.topAnchor.constraint(
        equalTo: subLabel.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    tableInput.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    tableInput.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4).isActive = true

    multiplierInput.topAnchor.constraint(
        equalTo: tableInput.bottomAnchor,
        constant: view.frame.height * 0.02
    ).isActive = true
    multiplierInput.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    multiplierInput.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4).isActive = true
}

You might have noticed this looks very much like setupLabels. That’s because autolayout requires a great deal of boilerplate but necessary code. I haven’t found a good way to reduce the plates on my boiler for this kind of code, so I end up with a fair amount of it when laying things out programmatically.

Now let’s create setupModes:

func setupModes() {
    view.addSubview(normalMode)
    view.addSubview(normalLabel)
    view.addSubview(randomMode)
    view.addSubview(randomLabel)
    
    normalMode.topAnchor.constraint(
        equalTo: multiplierInput.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    normalMode.leadingAnchor.constraint(equalTo: tableInput.leadingAnchor).isActive = true
    normalMode.addTarget(self, action: #selector(modeTapped), for: .touchUpInside)
    
    normalLabel.leadingAnchor.constraint(
        equalTo: normalMode.trailingAnchor,
        constant: view.frame.width * 0.01
    ).isActive = true
    normalLabel.centerYAnchor.constraint(equalTo: normalMode.centerYAnchor).isActive = true
    normalLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
    
    randomMode.topAnchor.constraint(
        equalTo: normalMode.bottomAnchor,
        constant: view.frame.height * 0.02
    ).isActive = true
    randomMode.leadingAnchor.constraint(equalTo: tableInput.leadingAnchor).isActive = true
    randomMode.addTarget(self, action: #selector(modeTapped), for: .touchUpInside)
    
    randomLabel.leadingAnchor.constraint(
        equalTo: randomMode.trailingAnchor,
        constant: view.frame.width * 0.01
    ).isActive = true
    randomLabel.centerYAnchor.constraint(equalTo: randomMode.centerYAnchor).isActive = true
    randomLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
}

Once again, very similar to the previous ones with one difference. We’re attaching a label to the right edge of each switch. So normalMode has a corresponding normalLabel to explain to the user what the switch is for. The normalLabel.leadingAnchor is constrained to the normalMode.trailingAnchor with some spacing given to constant.

Finally, we’ll set up the start button with setupStartButton:

func setupStartButton() {
    view.addSubview(startButton)

    startButton.topAnchor.constraint(
        equalTo: randomMode.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    startButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
}

Try it out!

Now that we have all the UI elements created and laid out and constrained, they should fall into place! On the simulator it’s looking good:

The app running on Xcode iPhone Simulator

In the next post, we’ll add some logic so our elements actually do things.

See you next time!

Leave a Reply

Your email address will not be published. Required fields are marked *