跳转至

9 Animating Auto Layout Constraints

So far, you’ve been creating static constraints and learning how to structure your app’s UI using Auto Layout. But did you know that you can also animate your constraints? It’s true — and in this chapter, you’ll learn how to do it.

With animations, you can:

  • Give feedback to the users.
  • Direct the attention to a specific part of the app.
  • Help the user to identify connections between parts of the app.
  • Create a better look and feel.
  • Improve navigation.

When done properly, animations can increase user engagement and are often what makes (or breaks) your app’s success. When creating animations, you need to consider three things: start value, end value and time.

Animate Auto Layout using Core Animations

You can animate constraints either by changing the constant of a constraint or by activating and deactivating constraints. The method you choose depends largely on what you’re trying to accomplish.

In this section, you’ll play with constraint animations by updating the MessagingApp project. Specifically, you’ll add new functionality to the app that allows users to like messages and mark them as favorite. Here’s how it’ll work: When a user double-taps a message, the app will show a toolbar with buttons to like the messages and to mark the message as favorite. This functionality will only work for messages not sent by the user.

To begin, open the MessagingApp project and build and run it.

img

The app displays a chat between two users; the messages on the right are the ones sent by you. If you double-tap one of the blue bubble messages now, nothing happens. But that’s about to change!

Setting up the delegate for MessageBubbleTableViewCell

Open MessageBubbleTableViewCell.swift and add the following code above the class declaration:

protocol MessageBubbleTableViewCellDelegate {
  func doubleTapForCell(_ cell: MessageBubbleTableViewCell)
}

This creates a new protocol that lets you define what happens when the user double-taps a cell.

Add this property at the top of the class:

var delegate: MessageBubbleTableViewCellDelegate?

This property allows the cell to remember its delegate.

Next, add the following new method and place it anywhere within the class:

@objc func doubleTapped() {
    delegate?.doubleTapForCell(self)
 }

In a moment, you’ll connect the double-tap gesture to this method. When this method is called, it notifies the delegate of the double tap.

You’re almost done with the delegate set up. In awakeFromNib(), below super.awakeFromNib(), add the following code:

let gesture = UITapGestureRecognizer(
  target: self, 
  action: #selector(doubleTapped))
gesture.numberOfTapsRequired = 2
gesture.cancelsTouchesInView = true
contentView.addGestureRecognizer(gesture)

This code adds a new gesture recognizer to the cell, specifically to the content view. Every time the user double-taps the content view, doubleTapped() gets called.

Go to MessagesViewController.swift and declare the following two properties at the top:

private let toolbarView = ToolbarView()
private var toolbarViewTopConstraint: NSLayoutConstraint!

This instantiates a ToolbarView and provides a way for you to keep track of its topconstraint. This is the constraint you’re going to animate, so you need to be able to access it later.

The project already includes a custom view named ToolbarView; you’ll use this view to show the like and favorite buttons to the user. The specific details of the implementation are not important for this exercise; all you need to know is that it’s a simple view containing a stack view and two buttons: one to like the message and the other to mark it as a favorite.

Below loadMessages(), add the following code:

private func setupToolbarView() {
  //1
  view.addSubview(toolbarView)

  //2
  toolbarViewTopConstraint = 
    toolbarView.topAnchor.constraint(
      equalTo: view.safeAreaLayoutGuide.topAnchor, 
      constant: -100)

  toolbarViewTopConstraint.isActive = true

  //3  
  toolbarView.leadingAnchor.constraint(
    equalTo: view.safeAreaLayoutGuide.leadingAnchor, 
    constant: 30).isActive = true
}

With this code, you:

  1. Add toolbarView to view, which makes it part of the view hierarchy.
  2. Set up toolbarViewTopConstraint, which positions toolbarView 100 points from the top. This position essentially hides the toolbar.
  3. Set up the leading constraints for toolbarView. Inside the initialization of ToolbarView, the width and height constraints are already set up, so you only need to indicate the horizontal and vertical constraints.

You now need to call setupToolbarView() when the view loads. In viewDidLoad(), below the call to loadMessages(), add the following:

setupToolbarView()

The next step is to have the toolbar appear when the user double-taps a message bubble. To accomplish this, you need to set up MessageBubbleTableViewCellDelegate.

First, add the following line before the return cell statement in tableView(_:cellForRowAt:):

cell.delegate = self

Next, add the following new extension to MessagesViewController:

extension MessagesViewController: MessageBubbleTableViewCellDelegate {
  func doubleTapForCell(_ cell: MessageBubbleTableViewCell) {
    //1
    guard let indexPath = self.tableView.indexPath(for: cell)
      else { return }
    let message = messages[indexPath.row]
    guard message.sentByMe == false else { return }

    //2
    toolbarViewTopConstraint.constant = cell.frame.midY

    //3
    toolbarView.alpha = 0.95

    //4
    toolbarView.update(
      isLiked: message.isLiked, 
      isFavorited: message.isFavorited)

    //5
    toolbarView.tag = indexPath.row

    //6
    UIView.animate(
      withDuration: 1.0,
      delay: 0.0,
      usingSpringWithDamping: 0.6,
      initialSpringVelocity: 1,
      options: [],
      animations: {
        self.view.layoutIfNeeded()
      },
      completion: nil)
  }
}

With this code, you:

  1. Ensure the message isn’t one sent by the local user.
  2. Change the value of toolbarViewTopConstraint.constant to cell.frame.midY.
  3. Set an alpha of 0.95 to toolbarView.
  4. Update the buttons of toolbarView. For example, if the message was already liked, the button is filled in.
  5. Set toolbarView.tag to equal the indexPath.row. This allows you to identify the message.
  6. Reproduce an animation. Don’t focus too much on all of the parameters of the call to UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:). Later, you can change the duration, delay and other properties, giving you a different visual experience of the animation.

The call to self.view.layoutIfNeeded() forces an Update Pass, so Auto Layout has to satisfy the new constraints. The Update Pass is part of the Render Loop — the process responsible for rendering — and keeps the UI up to date using the constraints. If you want to know more about this process, refer to Chapter 15, “Optimizing Auto Layout Performance.”

Build and run.

Double-tap one of the blue bubbles and the toolbar appears over the corresponding message.

img

Tap any of the buttons on the toolbar, and you’ll see its icon gets filled in with a color.

img

At the moment, there’s no way to hide the toolbar after it appears. To implement the hide functionality, add the following code after setupToolbarView():

@objc func hideToolbarView() {
  //1
  self.toolbarViewTopConstraint.constant =  -100

  //2
  UIView.animate(
    withDuration: 1.0,
    delay: 0.0,
    usingSpringWithDamping: 0.6,
    initialSpringVelocity: 1,
    options: [],
    animations: {
      self.toolbarView.alpha = 0
      self.view.layoutIfNeeded()
  },
  completion: nil)
}

With this code, you:

  1. Change the value of constant to -100 on toolbarViewTopConstraint, causing toolbarView to move out of sight as it will now be -100 points from the top.
  2. Set up a call to UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:).
  3. Set the alpha of toolbarViewto 0, causing the view to slowly disappear as part of the animation.
  4. Call self.view.layoutIfNeeded() so that a layout pass is scheduled.

Next, add the following code at the end of viewDidLoad():

let gesture = UITapGestureRecognizer(
  target: self, 
  action: #selector(hideToolbarView))
gesture.numberOfTapsRequired = 1;
gesture.delegate = self
tableView.addGestureRecognizer(gesture)

This code adds a gesture recognizer to tableview. When the user taps on the table view itself, hideToolbarView() is called. It also sets the delegate of the gesture to self.

To conform to this protocol, create a new extension at the bottom of the file, by adding the following code:

extension MessagesViewController: UIGestureRecognizerDelegate {
  func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer, 
    shouldReceive touch: UITouch
  ) -> Bool {
    return touch.view == tableView
  }
}

Here, you implement gestureRecognizer(_:shouldReceive:). Notice that this method returns the result of comparing touch.view to tableView. This guarantees that hideToolbarView() gets called only when the user taps on the tableView, not any of its children.

Build and run. Tap any of the blue message bubbles and then tap something else. Notice how the toolbar gracefully disappears.

img

Keep the application running and try this:

  1. Double-tap any of the blue bubbles, and then tap either the like or favorite button.
  2. Tap outside the bubble.
  3. Double-tap again over the previous bubble.

Sure enough, the state isn’t updating. In other words, the previously selected option isn’t getting saved.

Go to MessagesViewController and add the following line at the end of setupToolbarView():

toolbarView.delegate = self

If you look at ToolbarView.swift, you’ll see that it defines a delegate protocol which allows it to notify a delegate when the user taps either button in the toolbar. With the line above, you declare that MessagesViewController is the toolbar’s delegate.

Next, add the following new extension at the bottom of MessagesViewController.swift:

extension MessagesViewController: ToolbarViewDelegate {
  func toolbarView(
    _ toolbarView: ToolbarView, 
    didFavoritedWith tag: Int
  ) {
    messages[tag].isFavorited.toggle()
  }

  func toolbarView(
    _ toolbarView: ToolbarView, 
    didLikedWith tag: Int
  ) {
    messages[tag].isLiked.toggle()
  }
}

With this code, you implement the delegate methods for toolbarView. Now, when the user taps one of the buttons on the toolbar, the value of isFavorite and isLikedgets updated for that message. This happens thanks to the tag parameter, which represents the position of the specific message in messages array.

Build and run. Tap one of the blue bubble messages and tap either the Favorite or Like button. Now, tap outside of that message, and double-tap the same message again.

img

Notice, you’re now saving the state.

Congratulations, you just created a better user experience by using constraint animations.

Key points

  • Remember, you can activate and deactivate constraints to create animations.
  • Use animations to create a more engaging user experience.
  • To force Auto Layout to satisfy the new constraints, call layoutIfNeeded() on the affected view.