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.

4.7 (40) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. The leading and trailing are chained, like in previous examples.
  2. 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! :]

Swift mascot create UI layout on iPhone - SnapKit

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:

  1. Determine the current orientation of the device
  2. Use updateConstraints(_:) and update the timer label’s height to 45 if it’s in portrait — otherwise, you set it to 65.
  3. 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.

SnappyQuiz in landscape 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.