SnapKit for iOS: Constraints in a Snap
In this tutorial you’ll learn about SnapKit, a lightweight DSL (domain-specific language) to make Auto Layout and constraints a breeze to work with. By Shai Mishali.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SnapKit for iOS: Constraints in a Snap
20 mins
- Getting Started
- Snappin’ & Chainin’
- What is a DSL?
- SnapKit Basics
- Composability & Chaining
- Your First Constraints
- Do That Again
- A Quick Challenge!
- Final Constraint
- Modifying Constraints
- Updating a Constraint’s Constant
- Remaking Constraints
- Keeping a Reference
- When Things Go Wrong
- Where to Go From Here?
Do That Again
On to the next UI element — the question label. Find the following code:
lblQuestion.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lblQuestion.topAnchor.constraint(equalTo: lblTimer.bottomAnchor, constant: 24),
lblQuestion.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
lblQuestion.trailingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
])
There are three constraints here. Replacing these one-by-one might feel familiar at this point. The first constraint could easily be translated to:
make.top.equalTo(lblTimer.snp.bottom).offset(24)
And the final two constraints could also be translated in the same direct manner:
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
But actually, did you notice these two constraints do the same thing for the leading
and trailing
anchors? Sounds like a prefect fit for some chaining! Replace the entire code block from above with the following:
lblQuestion.snp.makeConstraints { make in
make.top.equalTo(lblTimer.snp.bottom).offset(24)
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
}
Note two things:
- The
leading
andtrailing
are chained, like in previous examples. - You don’t have to always use
snp
to constrain views! Note how, this time, your code simply creates a constraint to a good ol’UILayoutGuide
.
Another interesting fact is that the inset
option doesn’t have to be numeric. It can also take a UIEdgeInsets
struct. You could rewrite the line above as:
make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
.inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
This might not be too useful here, but can become extremely useful when the insets are different around the edges.
Two constraints down, three more to go!
A Quick Challenge!
The next constraint is one you’ve already seen before — the message label’s edges should simply equal to the superview’s edges. Why don’t you try this one yourself?
If you’re stuck, feel free to tap the button below to see the code:
[spoiler title=”Constrain edges to superview”]
Replace the following:
lblMessage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lblMessage.topAnchor.constraint(equalTo: navView.topAnchor),
lblMessage.bottomAnchor.constraint(equalTo: navView.bottomAnchor),
lblMessage.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
lblMessage.trailingAnchor.constraint(equalTo: navView.trailingAnchor)
])
With:
lblMessage.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
[/spoiler]
Final Constraint
There’s still one final constraint to move to SnapKit’s syntax. The horizontal UIStackView
holding the True and False buttons.
Find the following code:
svButtons.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
svButtons.leadingAnchor.constraint(equalTo: lblQuestion.leadingAnchor),
svButtons.trailingAnchor.constraint(equalTo: lblQuestion.trailingAnchor),
svButtons.topAnchor.constraint(equalTo: lblQuestion.bottomAnchor, constant: 16),
svButtons.heightAnchor.constraint(equalToConstant: 80)
])
Like before, the leading and trailing constraints could be chained since they are responsible for the same relationship. But since you don’t want to create a constraint to the superview, what should this look like?
Replace the code above with the following:
svButtons.snp.makeConstraints { make in
make.leading.trailing.equalTo(lblQuestion)
make.top.equalTo(lblQuestion.snp.bottom).offset(16)
make.height.equalTo(80)
}
Notice the first line in the makeConstraints
closure — you simply define that the leading and trailing constraints should equal to lblQuestion
! No specificity needed! SnapKit is able to infer that you’re referring to those specific constraints for lblQuestion
.
This is also true for simpler constraints. The following code:
view.snp.makeConstraints { make in make.width.equalTo(otherView.snp.width) make.centerX.equalTo(otherView.snp.centerX) }
Could be rewritten as:
view.snp.makeConstraints { make in make.width.equalTo(otherView) make.centerX.equalTo(otherView) }
Note that the specificity of otherView
is not needed — SnapKit knows what kind of constraints it needs to create based on the first view in the relationship.
You could even further reduce the code size by simply writing:
view.snp.makeConstraints { make in
make.width.centerX.equalTo(otherView)
}
Wow! How cool is that?
Build and run the project. You’ll notice that it still works just as it did before. Great! :]
Modifying Constraints
In the previous sections of this tutorial, you learned about creating new constraints. But, sometimes you want to modify an existing constraint.
Time to experiment with a few use cases where you might want to do this, and how to achieve this within SnapKit.
Updating a Constraint’s Constant
Some of SnappyQuiz’s users have been quite frustrated with how the app looks when switched to landscape orientation.
You can make it better by modifying some aspects of the UI when the app switches orientation, so you’ll do just that.
For this task, you’ll increase the height of the countdown timer in landscape orientation and also increase the font size. In this specific context, you need to update the constant
of the timer label’s height constraint.
When you’re only interested in updating a constant, SnapKit has a super-useful method called updateConstraints(_:)
, which makes for a perfect fit here.
Back in QuizViewController+Constraints.swift, add the following code at the end of the file:
// MARK: - Orientation Transition Handling
extension QuizViewController {
override func willTransition(
to newCollection: UITraitCollection,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.willTransition(to: newCollection, with: coordinator)
// 1
let isPortrait = UIDevice.current.orientation.isPortrait
// 2
lblTimer.snp.updateConstraints { make in
make.height.equalTo(isPortrait ? 45 : 65)
}
// 3
lblTimer.font = UIFont.systemFont(ofSize: isPortrait ? 20 : 32, weight: .light)
}
}
This adds an extension which will handle rotation of the view controller. Here’s what the code does:
- Determine the current orientation of the device
- Use
updateConstraints(_:)
and update the timer label’s height to 45 if it’s in portrait — otherwise, you set it to 65. - Finally, increase the font size accordingly depending on the orientation.
Thought it would be hard? Sorry to disappoint you! ;]
Build and run the project. Once the app starts in the Simulator, press Command-Right Arrow or Command-Left Arrow to change the device orientation. Notice how the label increases its height and font size based on the device’s orientation.
Remaking Constraints
Sometimes, you’ll need more than simply modifying a few constants. You might want to completely change the entire constraint set on a specific view. For that very common case, SnapKit has another useful method called — you guessed it — remakeConstraints(_:)
.
There’s a perfect place to experiment with this method inside SnappyQuiz: the progress bar on top. Right now, the progress bar’s width constraint is saved in a variable called progressConstraints
in QuizViewController.swift. Then, updateProgress(to:)
simply destroys the old constraint and creates a new one.
Time to see if you can make this mess a bit better.
Back in QuizViewController+Constraints.swift, take a look at updateProgress(to:)
. It checks if there is already a constraint and, if so, deactivates it. Then, it creates a new constraint and activates it.
Replace updateProgress(to:)
with the following:
func updateProgress(to progress: Double) {
viewProgress.snp.remakeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.width.equalToSuperview().multipliedBy(progress)
make.height.equalTo(32)
make.leading.equalToSuperview()
}
}
Whoa, this is much nicer! That entire somewhat-cryptic code piece was entirely replaced with just a few lines of code. remakeConstraints(_:)
simply replaces the entire constraint set every time, so you don’t have to manually reference the constraints and manage them.
Another upside of this is that you can further clean up some of the mess in the current code.
In setupConstraints()
, remove the following code:
guard let navView = navigationController?.view else { return }
viewProgress.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
viewProgress.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
viewProgress.heightAnchor.constraint(equalToConstant: 32),
viewProgress.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])
The first line in that method should now be simply updateProgress(to: 0)
.
Finally, you can get rid of the following lines in QuizViewController.swift:
/// Progress bar constraint
var progressConstraint: NSLayoutConstraint!
All done! Build and run your app, and everything should work as before, but with a much clearer constraints management code.