How To Add Google AdMob To Your iOS App

Google AdMob is an advertisement network for mobile apps, offered by Google. Basically, if you want to show some ads in your app and make money, you can use AdMob for that. Of course there are other options, but Google is certainly a trusted name in the business. Today we’ll look at how to add AdMob to an iOS app that I created for practicing times tables. You can see how the app was created here:

iOS Times Tables App – Part 1

Let’s get started!

Create a new app in AdMob

If you haven’t signed up for AdMob yet, check out how I did that for the Android version of this app. Assuming you have already signed up, go to https://admob.google.com/home/. We’re going to create a new app, so from the home page click “Add App”:

AdMob Home Page

Then we’re going to give AdMob a few basic details about the app:

App successfully created!

The app now has its own page in AdMob. The plan is to create a basic banner on the practice page, so we’ll create a single “Ad Unit”. Click on “Add Ad Unit”:

Make sure to select “Banner” on this page:

Give it a name on the next page and click “Create ad unit”:

We’ve successfully created an app and a place to show ads within it! Now on to the next step, integrating the Google AdMob SDK.

Installing the AdMob SDK

We’re going to do this with CocoaPods, as it’s the easiest and quickest way (in my humble opinion) to get external stuff. If you don’t have CocoaPods, check out their installation guide. Assuming CocoaPods is installed, we’re going to add CocoaPods to the Times Tables Xcode project that we used to create the app by creating a Podfile. CocoaPods describes this file in their Adding Pods to an Xcode Project guide.

From the terminal, cd your way to root directory of the app. Create a file called Podfile that looks like this, according to the Google AdMob SDK quick start guide:

target 'TimesTables' do

pod 'Google-Mobile-Ads-SDK'

end

If you haven’t updated CocoaPods in a while, you might need to update:

sudo gem install cocoapods

Then run this command to install the SDK:

pod install --repo-update

At this point, the SDK should be installed! You’ll now need to abandon the project file and start using the newly created workspace file called TimesTables.xcworkspace.

The next step is to edit the project’s Info.plist file to include your AdMob app ID. Following the same Google AdMob SDK quick start guide at section “Update your Info.plist”, we’re going to copy and paste the long XML snippet they show there into Info.plist.

You can edit the plist file from Xcode, but I found it hard to copy and paste anything. I’d recommend just opening with an external editor like TextEdit and pasting it in that way. Info.plist is not in the app’s root directory, it’s in a directory that should have the name of your app that holds your swift files. The directory for this particular app is called TimesTables.

You’ll need to find your AdMob app ID from the AdMob console and replace the dummy one they show here. Your app ID can be found from the “All Apps” AdMob page I showed at the very top of this post.

Now that we have made the required plist changes, we’ll add this snippet to didFinishLaunchingWithOptions in AppDelegate.swift, or more likely, if you’re on iOS 13 or above it’ll be in the SceneDelegate.swift in willConnectTo:

GADMobileAds.sharedInstance().start(completionHandler: nil)

Make sure you import the SDK at the very top of the file to which you added the above snippet:

import GoogleMobileAds

We have successfully downloaded and installed the AdMob SDK in our app!

Showing ads in the ad unit

Our next step is to actually show some ads in the ad unit we created in AdMob. Following Google’s AdMob iOS guide, we’ll add a new global variable to hold the banner. Add this to the top of the TableViewController (see the app code if you need) like this:

import GoogleMobileAds //Import the SDK

class TimesTableViewController: UIViewController {

var bannerView: GADBannerView = GADBannerView(adSize: GADAdSizeBanner) //Create the banner

//class continues from here

Then we’ll write a function to set it up. I used programmatic constraints with autolayout here but you can do anything you want to position it:

func setupBanner() {
    view.addSubview(bannerView) // Add to the view
    bannerView.translatesAutoresizingMaskIntoConstraints = false //Turn on autolayout
    bannerView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true // X: same as view center
    bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true // Y: attach bottom to view bottom
    bannerView.adUnitID = "ca-app-pub-3940256099942544/6300978111" //Google test banner ad unit ID
    bannerView.rootViewController = self //Google says do this so we do it
    bannerView.load(GADRequest()) //load an ad
}

Make sure to call this function from viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    setupBanner()
    //There's more stuff here in the actual app, but you get the idea.
}

Try it out!

Our banner is looking great!

The Times Tables App running in the iOS simulator

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

iOS Swift – Building a Times Tables App – Part 3

In two previous posts, we first added the UI elements and then added a bit of logic to make the initial view of the app work. You can find those posts here:

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

The full code for the app can be found on my Github page:

https://github.com/jamesmcclay/swift_times_tables

Today we’re going to add a UINavigationController as well as a second view controller for the actual times tables flash card view.

Adding a UINavigationController

UINavigationController is a handy object available in UIKit. It allows us to put a navigation bar that persists on the screen even as the user changes from one UIViewController to another. By default, it will come with a functioning “back” button.

Since we’re taking a programmatic approach for pretty much everything except adding UIViewControllers, we need to delete the storyboard entrypoint and add one from code. It shows up as a big arrow pointing to the initial UIViewController:

Storyboard Entry Point in Xcode

Once we have it selected (you can click on either the arrow on the canvas or on the one to the left), just hit delete and it’s gone. While we’re in the storyboard, make sure the main view controller has “Storyboard ID”, you can add it from the identity inspector. I called mine “homeVC”:

Identity Inspector for UIViewController

The next step is to add the UINavigationController to the app as well as an entry point from code. If you are developing for iOS 13 and onward, the place to do this is in SceneDelegate.swift. It used to be AppDelegate.swift, but no longer. We’ll add our code to SceneDelegate, in the function willConnectTo. The top of my SceneDelegate.swift file looks like this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow? //Gotta add this reference to the app window

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }

        //Adding the UINavigationController here
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController (withIdentifier: "homeVC") as! ViewController
        let navigationController = UINavigationController(rootViewController: vc)
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        //End of custom code

        guard let _ = (scene as? UIWindowScene) else { return }
    }

In the first couple lines, we grab references to the storyboard and the home UIViewController. Then we create a UINavigationController, and add the home UIViewController as the root view controller. In the last three lines, we start up the window, make it visible and make the UINavigationController the root view controller for the window (yes, two roots). That should replace the storyboard entrypoint as well as add a navigation controller.

Add a second UIViewController

We’re going to use the storyboard to add a second UIViewController. Click the “+” button at the top left and a view controller:

Adding a UIViewController from the storyboard

Then we need to connect to it a swift file. Over on the left side where all the rest of the project files are, right click on the “view” folder and click “new file”. Create a “Cocoa Touch Class”:

Creating a Cocoa Touch Class

Let’s call it “TimesTableViewController”. Click through and create. The last step is to connect the newly created swift file to the newly created UIViewController on the storyboard. We can do that from the storyboard by clicking on the UIViewController and going to its identity inspector and changing the class to the new class we just created, and be sure to give it a storyboard ID:

Connecting a swift file to UIViewController

And our second UIViewController is complete. Now we just need to add code to jump to this view (or rather, push it on to the UINavigationController, to be precise) when the user taps the start button. Please check the github code or previous posts if you’re not sure what I’m talking about here. Let’s add this snippet to startButtonTapped:

let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let ttvc: TimesTableViewController = storyBoard.instantiateViewController(withIdentifier: "ttVC") as! TimesTableViewController
self.navigationController!.setViewControllers([self,ttvc], animated: true)

This looks pretty complicated, but it’s mostly just some magic that Apple wants you to do when you’re pushing a new view controller to the navigation controller. Lastly, I didn’t find that I wanted the navigation bar to show up on the home view controller, just the times tables one. So I added this to my code in the (home) ViewController:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(true)
    self.navigationController!.navigationBar.isHidden = true
}

This just tells the navigation bar to be hidden when the view appears.

Try it out!

The main view is unchanged, but when we click start, we head to the next view! And there’s a back button! And it works!

Hope you liked it. Next time we’ll code in the times tables view. Stay tuned!

iOS Swift – Building a Times Tables App – Part 2

Last post, we put together the UI layout for a times times tables app in Swift using Xcode. The UI elements all work and are in their correct positions, but there’s no logic to make them do anything. Today we’ll add a dash of logic to the home screen so that we can gather up all the information we need to show the user the times tables flash cards they want to see. You can find parts 1 and 3 of this series here:

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

To really understand what I’m talking about in this post, you’ll either want to look at the last post (link above) or take at the code for the ViewController in my github account:

https://github.com/jamesmcclay/swift_times_tables

Adding variables for selected values

There are three different values that the user could potentially provide to the app, and we need to account for all of them. The first is the table – for example, if the user wanted to practice 3×2, 3×3, 3×4 and so on, they would provide 3 in the “table” field. The second is the max multiplier, which is the second number. In the Android version, we set the default to be 12, we’ll keep with that. But the user can change the default by entering something in the max multiplier field. The last is the mode – normal or random. We’ll add some global variables at the top of the class, just below the UI elements variables:

var currentMode:String {
    get {
        if let mode = UserDefaults.standard.value(forKey: "mode") as? String {
            return mode
        } else {
            return "normal"
        }
    }
    set {
        UserDefaults.standard.setValue(newValue, forKey: "mode")
    }
}
var currentTable:Int {
    get {
        if let table = UserDefaults.standard.value(forKey: "table") as? Int {
            return table
        }else{
            return 0
        }
    }
    set {
        UserDefaults.standard.setValue(newValue, forKey: "table")
    }
}
var currentMultiplier:Int {
    get {
        if let multiplier = UserDefaults.standard.value(forKey: "multiplier") as? Int {
            return multiplier
        }else{
            return 0
        }
    }
    set {
        UserDefaults.standard.setValue(newValue, forKey: "multiplier")
    }
}

If you’re not familiar with “computed properties” in Swift, this probably looks pretty foreign. Basically, a computed property is a variable that doesn’t hold a value. Normally, variables hold values. Instead, you write code to get and set the value. The get and set code runs when elsewhere in your code you write something that either gets or sets the variable’s value. That’s why there’s a get and set part (technically set is optional).

The reason we’re doing this here is because we want the values to be stored in the UserDefaults file. UserDefaults is just a text file where you can store some key-value data that you can access even after the app or phone restarts. Because of that persistence, the app can “remember” values that were entered last time so the user can avoid re-entering their favorite table, max multiplier or mode. We’ll also be able access the values from the next view in the app with the actual flash cards without having to “send” it like in Android. For getting, it also lets us account for the possibility that there will be no value in UserDefaults, and return a default in that case. The get blocks return “normal” by default for the mode, and 0 by default for the table and multiplier.

Populate the table and max multiplier

We’ll use the code we just wrote to put values in the table and max multiplier text fields. In the setupInputs function we’ll add the following snippet:

if currentTable != 0 {
    tableInput.text = String(currentTable)
}
if currentMultiplier != 0 {
    multiplierInput.text = String(currentMultiplier)
}

Remember, since currentTable and currentMultiplier are computed, these lines simply run the get blocks that we wrote earlier for these variables. If the values stored in UserDefaults were not 0, we’re setting the the textfields to be whatever was stored. The reason why we avoid pre-populating 0 is because we’ll use 0 in the startTapped function to mean that the user accepted the defaults and entered nothing. In that case, we don’t need to pre-populate anything. We’ll add this snippet to startTapped:

if let tableText = tableInput.text {
    if let table = Int(tableText) {
        currentTable = table
    }else{
        currentTable = 0
    }
}else{
    currentTable = 0
}
if let multiText = multiplierInput.text {
    if let multiplier = Int(multiText) {
        currentMultiplier = multiplier
    }else{
        currentMultiplier = 0
    }
}else {
    currentMultiplier = 0
}

This code, which runs when the user taps the start button, will grab whatever the user entered in the text fields and runs the set blocks that we wrote earlier. We check first to see if there’s any text at all, then to see if it will properly change to an Integer. If both of those pass, we set the entered value to UserDefaults. If either fails, we set the UserDefault value to 0.

Setting the mode

In Android, we were able to create a radio group with two multually exclusive options, normal and random. In Swift and UIKit, no such thing exists. So what we’ll use is UISwitch, which is a little switcher button thingy. We already laid them out in the last post, but we need to make sure they work right. When one is selected, the other can’t also be selected. We’ll achieve that with the following code in modeTapped:

modes.forEach { $0.isOn = false }
sender.isOn = true
switch sender.tag {
case 0:
    currentMode = "normal"
case 1:
    currentMode = "random"
default:
    currentMode = "normal"
}

That first line simply unchecks both of the UISwitch elements in the modes array. Remember creating that array? It just holds the switches so we can loop through them. Then we turn the switch on for whichever switch fired the function, followed by setting currentMode to either “normal” or “random” based on whether the sender’s tag is 0 or 1. We set the tags when we created the switches, you might have forgotten 🙂

We’ll “remember” whatever setting the user selected last time they opened the app by adding this snippet to setupModes:

if currentMode == "random" {
    randomMode.isOn = true
}else{
    normalMode.isOn = true
}

This once again pulls the mode value from UserDefaults.

Dismiss keyboard

I ran into an issue when I realized I couldn’t dismiss the keyboard once it appeared for the text fields. I got it to go away by adding UITextFieldDelegate to my ViewController’s class declaration so it looks like this:

class ViewController: UIViewController, UITextFieldDelegate {

And for each text field, made the ViewController the delegate in the setupInputs function:

tableInput.delegate = self
multiplierInput.delegate = self

Finally, I added this function which is available if your ViewController has been made the UITextFieldDelegate:

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}

It basically just tells the keyboard to go away when return is tapped, if they keyboard appeared for a text field.

Try it out!

Our app in the Xcode iOS simulator

Not a whole lot about the look has changed since the last post, but one thing you can do is print the values of currentTable, currentMultiplier and currentMode in the console. You can also enter some, press start, and then close/restart the app. You should be able to see the values persist.

Stay tuned for the next post, where we’ll create the screen for the flash cards!

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!