Day 36 - 100 Days of Swift

5 minute read

Project 8 (part 1)

Day 36 is the first part of the eighth project. It is a game project, loosely based on “7 Little Words”, where the user tries to guess the word based on a provided clue and some word-chunk options. It is also the first iPad-first app we’ve built. Today is all about getting the UI laid out, which you do totally in code, mostly using auto layout constraints.

First you make a new app, like normal, but set the “devices” for the project to be iPad instead of Universal. Then you give yourself you properties to hold references to the views you are about to create:

1
2
3
4
5
var cluesLabel: UILabel!
var answersLabel: UILabel!
var currentAnswer: UITextField!
var scoreLabel: UILabel!
var letterButtons: [UIButton] = []

The rest of the work is done in loadView(), which you override to give your own implementation. First, you load the view itself:

1
2
view = UIView()
view.backgroundColor = .white

Then you add the scoreLabel:

1
2
3
4
5
scoreLabel = UILabel()
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.textAlignment = .right
scoreLabel.text = "Score: 0"
view.addSubview(scoreLabel)

And constrain it:

1
2
3
4
NSLayoutConstraint.activate([
        scoreLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
        scoreLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: 0),
])

You add all the constraints inside of NSLayoutContstraint.activate so that you don’t have to write .isActive = true on each one of them as you go. All the rest of the constraints that I show will be inside of that array.

Next, you add the labels for the clues and answers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cluesLabel = UILabel()
cluesLabel.translatesAutoresizingMaskIntoConstraints = false
cluesLabel.font = UIFont.systemFont(ofSize: 24)
cluesLabel.text = "Clues".uppercased()
cluesLabel.numberOfLines = 0
cluesLabel.setContentHuggingPriority(UILayoutPriority(1), for: .vertical)
view.addSubview(cluesLabel)

answersLabel = UILabel()
answersLabel.translatesAutoresizingMaskIntoConstraints = false
answersLabel.font = UIFont.systemFont(ofSize: 24)
answersLabel.text = "Answers".uppercased()
answersLabel.numberOfLines = 0
answersLabel.textAlignment = .right
answersLabel.setContentHuggingPriority(UILayoutPriority(1), for: .vertical)
view.addSubview(answersLabel)

And then constrain them:

1
2
3
4
5
6
7
8
// In NSLayoutConstraint.activate()
cluesLabel.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor),
cluesLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 100),
cluesLabel.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor, multiplier: 0.6, constant: -100),
answersLabel.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor),
answersLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: -100),
answersLabel.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor, multiplier: 0.4, constant: -100),
answersLabel.heightAnchor.constraint(equalTo: cluesLabel.heightAnchor),

Then you add the UITextField to display the answer the user is currently working on:

1
2
3
4
5
6
7
currentAnswer = UITextField()
currentAnswer.translatesAutoresizingMaskIntoConstraints = false
currentAnswer.placeholder = "Tap letters to guess"
currentAnswer.textAlignment = .center
currentAnswer.font = UIFont.systemFont(ofSize: 44)
currentAnswer.isUserInteractionEnabled = false
view.addSubview(currentAnswer)

And constrain it:

1
2
3
currentAnswer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
currentAnswer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
currentAnswer.topAnchor.constraint(equalTo: cluesLabel.bottomAnchor, constant: 20),

Then you add buttons for submitting the answer and for clearing it out:

1
2
3
4
5
6
7
8
9
let submit = UIButton(type: .system)
submit.translatesAutoresizingMaskIntoConstraints = false
submit.setTitle("Submit".uppercased(), for: .normal)
view.addSubview(submit)

let clear = UIButton(type: .system)
clear.translatesAutoresizingMaskIntoConstraints = false
clear.setTitle("Clear".uppercased(), for: .normal)
view.addSubview(clear)

And constrain them (noticing a pattern for programmatically laying out UIs?):

1
2
3
4
5
6
submit.topAnchor.constraint(equalTo: currentAnswer.bottomAnchor),
submit.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -100),
submit.heightAnchor.constraint(equalToConstant: 44),
clear.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 100),
clear.centerYAnchor.constraint(equalTo: submit.centerYAnchor),
clear.heightAnchor.constraint(equalToConstant: 44),

Finally, we add a UIView to hold the 20 buttons that will display the word fragments, and constrain that in place:

1
2
3
4
5
6
7
8
9
10
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(buttonsView)

// In NSLayoutConstraint.activate()
buttonsView.widthAnchor.constraint(equalToConstant: 750),
buttonsView.heightAnchor.constraint(equalToConstant: 320),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.topAnchor.constraint(equalTo: submit.bottomAnchor, constant: 20),
buttonsView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -20)

For the buttons themselves, you don’t use auto layout constraints, you just set their frames directly. Since buttonsView is a fixed size, this process is pretty easy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let width = 150
let height = 80

for row in 0...3 {
    for col in 0...4 {
        let letterButton = UIButton(type: .system)
        letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
        letterButton.setTitle("WWW", for: .normal)
        let frame = CGRect(x: col * width, y: row * height, width: width, height: height)
        letterButton.frame = frame
        buttonsView.addSubview(letterButton)
        letterButtons.append(letterButton)
    }
}

With all of that in place, it looks like this:

Screenshot of working app.

You can find my version of this project at the end of day 36 on Github here.

Reflections

I really like laying out UIs programmatically. It is very verbose, and it sometimes takes a while to find all the settings you need to set to get what you want, but at the same time it is very clear and explicit and reusable. It is super interesting to me to see how different people layout similar UIs in code, because if I was building this from scratch, I probably would have started with UIStackViews. It’s cool to see different methodologies. It also makes me even more pumped to start messing with SwiftUI, because I think it will make a lot of this stuff even easier.