iOS & Swift Tutorials

Learn iOS development in Swift. Over 2,000 high quality tutorials!

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.

4.8/5 8 Ratings

Version

  • Swift 5, iOS 12, Xcode 10

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. You’ll come away from this tutorial being able to write your layout code like a layout ninja would!

Auto Layout is a powerful tool to describe the relationships and constraints between different views and complex view hierarchies in your application, but writing these constraints can often be quite non-intuitive at first.

Up until a few years ago, writing these constraints programmatically was quite tedious with cryptic and verbose methods such as using Visual Formatting Language or manually creating NSLayoutConstraints.

iOS 9 greatly improved these mechanisms with the introduction of Layout Anchors, which make creating constraints quite intuitive and declarative. And yet, there is still much to be desired to make it even snappier for you to create constraints. This is exactly where SnapKit comes into play!

Getting Started

Throughout this tutorial, you’ll work on SnappyQuiz — a simple game where the player gets random questions/statements and picks whether they’re true or false.

Use the Download Materials button at the top or bottom of this tutorial to download the starter project for this tutorial, which already has SnapKit bundled. Open SnappyQuiz.xcworkspace and not the project file — this is important.

The project comprises a few Swift files you’ll need:

  • QuizViewController.swift: This is where the layout of the screen happens, including defining the views.
  • QuizViewController+Logic.swift: This file houses the logic of the game itself. You won’t need to change this file in this tutorial.
  • QuizViewController+Constraints.swift: All of the constraints of the screen’s UI are located in in this file, which is where you’ll do most of the work.

The project also includes State.swift which represents the game state and Questions.swift where the raw question data is found, but you won’t really touch these during this tutorial.

Build and run the project. You should see the first question with a countdown timer ticking as well as a progress bar representing the current game progress:

SnappyQuiz running

In QuizViewController+Constraints.swift, explore setupConstraints(). This code uses the aforementioned Layout Anchors to define the relations between the different views in the app. You can find more about Layout Anchors in our Easier Auto Layout tutorial.

In this tutorial, you’ll replace each of these constraints with its SnapKit variation.

Snappin’ & Chainin’

Before you actually modify the SnappyQuiz application, it’s time for you to learn a bit more about what SnapKit actually is. In the introduction to this tutorial, I mentioned SnapKit uses a DSL, but what does that actually mean?

What is a DSL?

A domain-specific language (DSL) is a language created to express and deal with a specific domain or to solve a specific problem.

In SnapKit’s case, it aims to create a syntax that is much more intuitive and easy-to-use specifically for Auto Layout constraints.

An important thing to understand is that, as a DSL, SnapKit is mostly syntactic sugar — you can do anything SnapKit does without SnapKit. However, SnapKit provides a much more fluent and expressive syntax to solve this specific domain and problem.

SnapKit Basics

Take a very common set of constraints — attaching a view to all of its superview’s edges:

Without SnapKit, the code would look similar to the following:

child.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
  child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
  child.topAnchor.constraint(equalTo: parent.topAnchor),
  child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
  child.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])

This is quite declarative, but SnapKit can do better.

SnapKit introduces a namespace called snp on every UIView (and NSView, on macOS) in your system. That namespace, along with the makeConstraints(_:) method, are the essence of SnapKit.

SnapKit represents those constraints like this:

child.snp.makeConstraints { make in
  make.leading.equalToSuperview()
  make.top.equalToSuperview()
  make.trailing.equalToSuperview()
  make.bottom.equalToSuperview()
}

This might seem like a similar amount of code, but it greatly improves readability. Two things you might’ve noticed are:

  1. You don’t need to reference parent at all, thanks to SnapKit’s equalToSuperview(). This means that, even if child moves to a different parent view, you won’t need to modify this code.
  2. The make syntax create an almost-English-like syntax, e.g. “make leading equal to superview“, which is much nicer to read.

Composability & Chaining

You just saw your first SnapKit code, but where SnapKit really shines is its composition capabilities. You can chain any anchors together, as well as the constraints themselves.

You can rewrite the example above as:

child.snp.makeConstraints { make in
  make.leading.top.trailing.bottom.equalToSuperview()
}

Or even more concisely as:

child.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

Want to add an inset of 16 to your view? Another simple chaining will get you there:

child.snp.makeConstraints { make in
  make.edges.equalToSuperview().inset(16)
}

As you can see, composability and chaining are at the core of SnapKit and provide expressiveness you simply can’t achieve with vanilla NSLayoutConstraints.

Your First Constraints

Now that you have some of the basics of SnapKit, it’s time for you to convert all of the constraints in setupConstraints() to use it. It’s much simpler than you’d expect, and you’ll go through these one-by-one, exploring SnapKit’s various capabilities.

Go back to QuizViewController+Constraints.swift and find setupConstraints(). You’ll start modifying constraints below the updateProgress(to: 0) line. You’ll go back to the constraints above that line later on.

Find the following block of code, defining constraints for the timer label:

lblTimer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblTimer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
  lblTimer.heightAnchor.constraint(equalToConstant: 45),
  lblTimer.topAnchor.constraint(equalTo: viewProgress.bottomAnchor, constant: 32),
  lblTimer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])

Replace it with the following:

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45) // 1
  make.height.equalTo(45) // 2
  make.top.equalTo(viewProgress.snp.bottom).offset(32) // 3
  make.centerX.equalToSuperview() // 4
}

Like before, this is a direct translation of the original constraints, using SnapKit’s chaining syntax. Quickly breaking this down:

  1. Make the label’s width equal to the superview’s width, multiplied by 0.45 (i.e., 45% of the superview’s width).
  2. Set the label’s height to a static 45.
  3. Constrain the top of the label to the bottom of the progress bar, offset by 32.
  4. Center the X axis to the superview’s X axis, making the label horizontally centered.

While not too different from the NSLayoutConstraint-based code, it provides much better readability and scoping of the views being constrained.

Note: Notice something else different? SnapKit no longer requires you to set translatesAutoresizingMaskIntoConstraints to false! The library does it for you. No more forgetting to do that and tirelessly debugging messed up constraints :].

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.

Keeping a Reference

While you won’t experiment with this option in SnappyQuiz, it’s still one you should know of.

In standard NSLayoutConstraint fashion, you can store a reference to your constraint and modify it later on. That’s also possible with SnapKit, using the Constraint type:

var topConstraint: Constraint?

lblTimer.snp.makeConstraints { make in 
  // Store your constraint
  self.topConstraint = make.top.equalToSuperview().inset(16)
  make.leading.trailing.bottom.equalToSuperView()
}

// Which you can later modify
self.topConstraint?.update(inset: 32)

// Or entirely deactivate
self.topConstraint?.deactivate()

When Things Go Wrong

Sometimes in life, things go wrong. This is even more often the case when talking about Auto Layout constraints.

Back in QuizViewController+Constraints.swift, find the following line:

make.centerX.equalToSuperview()

Right below it, but still inside the makeConstraints closure, add:

make.centerY.equalToSuperview()

Build and run the app. As you can see, the UI is entirely broken:

App with broken constraints

Also, as expected, you’ll see a giant wall of broken constraints in your debug console, which should look similar to the following:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x600001b251a0@QuizViewController+Constraints.swift#62 UIView:0x7f9371e004a0.top == UILayoutGuide:0x60000062c0e0.top>",
    "<SnapKit.LayoutConstraint:0x600001b25260@QuizViewController+Constraints.swift#64 UIView:0x7f9371e004a0.height == 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7f9371e088c0.height == 45.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dce0@QuizViewController+Constraints.swift#39 UILabel:0x7f9371e088c0.top == UIView:0x7f9371e004a0.bottom + 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dda0@QuizViewController+Constraints.swift#41 UILabel:0x7f9371e088c0.centerY == UIView:0x7f9371e09a50.centerY>",
    "<NSLayoutConstraint:0x600001c6c2d0 'UIView-Encapsulated-Layout-Height' UIView:0x7f9371e09a50.height == 551   (active)>",
    "<NSLayoutConstraint:0x600001c61450 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x60000062c0e0'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7f9371e09a50 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

Oh boy. Where do you even start? All you see is a bunch of memory addresses that don’t necessarily mean too much. It’s also quite difficult to understand which constraints were broken.

Luckily, SnapKit provides a great additional modifier to track down these sort of issues, called labeled(_:).

Replace the entire lblTimer constraint block with the following:

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45).labeled("timerWidth")
  make.height.equalTo(45).labeled("timerHeight")
  make.top.equalTo(viewProgress.snp.bottom).offset(32).labeled("timerTop")
  make.centerX.equalToSuperview().labeled("timerCenterX")
  make.centerY.equalToSuperview().labeled("timerCenterY")
}

Noticed the labeled(_:) addition on every constraint? This lets you attach a descriptive title for every constraint, so you don’t have to pick through memory addresses and lose your sanity.

Build and run your app one final time. Your broken constraints should provide much clearer information at this point:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x60000365c4e0@QuizViewController+Constraints.swift#62 UIView:0x7fc53e4181d0.top == UILayoutGuide:0x600002b0ae60.top>",
"<SnapKit.LayoutConstraint:0x60000365e8e0@QuizViewController+Constraints.swift#64 UIView:0x7fc53e4181d0.height == 32.0>",
"<SnapKit.LayoutConstraint:timerCenterY@QuizViewController+Constraints.swift#41 UILabel:0x7fc53e41d060.centerY == UIView:0x7fc4fe507170.centerY>",
"<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>",
"<SnapKit.LayoutConstraint:timerTop@QuizViewController+Constraints.swift#39 UILabel:0x7fc53e41d060.top == UIView:0x7fc53e4181d0.bottom + 32.0>",
"<NSLayoutConstraint:0x6000031346e0 'UIView-Encapsulated-Layout-Height' UIView:0x7fc4fe507170.height == 551   (active)>",
"<NSLayoutConstraint:0x600003139c70 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x600002b0ae60'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7fc4fe507170 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

This looks similar, but look carefully. You can see gems like timerCenterY. This is much more informative, and you have some great labeled constraints to start debugging your way through.

More specifically, the only three labels you can recognize in this output are timerCenterY, timerHeight and timerTop. Since the height is static, you can be sure the conflict is between the two constraints left. That narrowed things down much faster than picking through the original mess of Auto Layout debugging output!

Once you’re done, feel free to remove the centerY constraint that started this mess.

Where to Go From Here?

Congratulations! You now know most of what SnapKit has to offer, but there are still a few features and modifiers you should look into, such as priority, divided and more. Check out SnapKit’s official GitHub repo for more information.

Remember, SnapKit is there to help you by creating an easy-to-consume, problem-specific syntax for creating constraints, but it doesn’t provide features that can’t be achieved with regular NSLayoutConstraints. Feel free to experiment with both and find a good middle ground that works for each scenario.

We hope you’ve enjoyed this tutorial. Got any more questions or … constraints ? Leave a comment in the forum thread below.

Average Rating

4.8/5

Add a rating for this content

8 ratings

Contributors

Comments