Buddybuild is a CI, CD and user feedback platform built for mobile development teams - sign up for free and start building better apps!

Active Filters: Animation

Today we'll continue looking at UICollectionView's more advanced capabilities by checking out a little-known feature hiding inside UICollectionViewController.

It allows us to automatically animate the transition between layouts when pushing on to a navigation controller stack. Neat!

Let's give it a try.

We'll start with the same simple baseline collection view controller we used in Bite #306, and Bite #307, SquaresViewController.

It looks like this:

class SquaresViewController: UICollectionViewController {
  var items = [Item]()

  override func collectionView(
    _ collectionView: UICollectionView, 
    numberOfItemsInSection section: Int
  ) -> Int {
    return items.count
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath
  ) -> UICollectionViewCell {
    let cell = collectionView
      .dequeueReusableCell(
        withReuseIdentifier: "ItemCell", 
        for: indexPath
      )

    cell.backgroundColor = items[indexPath.item].color

    return cell
  }
}

Nothing fancy, just standard UICollectionViewController stuff.

Next, we'll subclass it twice. Once for our "small" cells:

class SmallViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: 50, height: 50)

    super.init(collectionViewLayout: layout)

    items = (0...50).map { _ in Item(color: .random()) }
  }

We'll fill it up with some random items, like we did previously, here in our "small" subclassed view controller.

Then, when a user selects one of the cells, we'll push on a "big" view controller, setting its items property to our own:

  override func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt indexPath: IndexPath
  ) {
    let bigVC = BigViewController()

    bigVC.items = items

    navigationController?
      .pushViewController(bigVC, animated: true)
  }
}

Again, nothing really too crazy here. Lastly, before we can try it out, lets make our BigViewController:

class BigViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()

    layout.itemSize = CGSize(width: 100, height: 100)

    super.init(collectionViewLayout: layout)
  }

We'll make our layout's itemSize property a little larger than before, then we'll override collectionView(_:didSelectItemAt:) one more time.

This time we'll simply call popViewController:

  override func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt indexPath: IndexPath
  ) {
    navigationController?.popViewController(animated: true)
  }
}

If we build and run now, everything works as expected, but there's no fancy transition happening. Just a regular navigation controller push.

Let's fix this.

We'll add one line of code to SmallViewController's init function, setting useLayoutToLayoutNavigationTransitions to false:

class SmallViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()    
    layout.itemSize = CGSize(width: 50, height: 50)

    super.init(collectionViewLayout: layout)

    useLayoutToLayoutNavigationTransitions = false
    // .. 
  }

  // ..

Then we'll do set the same property to the true in BigViewController:

class BigViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: 100, height: 100)

    super.init(collectionViewLayout: layout)

    useLayoutToLayoutNavigationTransitions = true
  }

  // ..

That's it. UIKit will notice that we've set our BigViewController's useLayoutToLayoutNavigationTransitions to true, and will automatically animate the transition between the two layouts.

Note that the automatic transition animation that UIKit renders is smart enough to make sure the selected cell is still visible after we push in. A nice touch.

It's always fun to discover the handy little behaviors hiding out in to UIKit.

Finally, a couple of important notes:

First, the same UICollectionView is actually reused when all of this magic happens. The pushed on view controller will not create its own. This may or may not matter for any given app, but good to know.

Second, the root view controller (SmallViewController in our case) will still be set as the delegate and dataSource when the new view controller is pushed on.

If we needed to change this behavior, we could conform to the UINavigationControllerDelegate protocol and change these values each time a different view controller is about to be be shown. The code might look something like this:

extension SquaresViewController : UINavigationControllerDelegate {
  func navigationController(
    _ navigationController: UINavigationController, 
    willShow viewController: UIViewController, 
    animated: Bool
  ) {
    guard let squaresVC = viewController as? SquaresViewController else { return }

    squaresVC.collectionView?.delegate = squaresVC
    squaresVC.collectionView?.dataSource = squaresVC
  }
}

That's all for today, have a specific UICollectionView question you'd like answered? Send it along!.

Download the Xcode project we built in this Bite right here.

Today we'll continue our look at UICollectionView by diving into animation. Let's begin.

We'll start with the same simple baseline collection view controller we made at the beginning of Bite #306.

Now, we'll add two new collection view layouts to our view controller:

var small: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 75, height: 75)

  return layout
}()

var big: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 150, height: 150)

  return layout
}()

Then, we'll override init on our view controller, and hand it our "small" layout:

init() {
  super.init(collectionViewLayout: small)
}

Nice. At this point we have a basic collection view controller, scrolling our small cells:

Now, we'll add some code to toggle between our two layouts when the user taps any cell:

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  let newLayout = (collectionView.collectionViewLayout == small ? big : small)

  collectionView.setCollectionViewLayout(newLayout, animated: true)
}

Now we can toggle back and forth between our two layouts with a nice looking animation. Very cool.

It's super handy how the setCollectionViewLayout function is smart enough to alter the animtion so the cell we selected is still in view once the animation completes.

We can see this effect exaggerated when we bump up the itemSize of our "big" layout:

var big: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 300, height: 300)

  return layout
}()

Nice!

Download the Xcode project we built in this Bite right here.

One final note here: This just one approach. There are tons of different ways to achieve this (and similar) behavior. We'll take a look at more as we dive deeper into collection views in upcoming Bites.

Have a specific UICollectionView question you'd like answered? Send it along!.

UICollectionView has always been a powerhouse. Its variety and versatility are tough to understate.

We've covered it a bit here on LBOC, but we've still only just scratched the surface. Today we'll begin to dive deeper in what all it can do by customizing how new collection view cells animate when they're being inserted. Let's begin!

We'll start by creating a new single view app in Xcode.

We'll drag out a UICollectionViewController in Interface Builder, set its class to be the ViewController one that comes with the Xcode template, then update ViewController.swift to have some basics:

class ViewController: UICollectionViewController {
  var items = [Item]()

  func addItem() { items.append(Item(color: .random())) }

  override func viewDidLoad() {
    super.viewDidLoad()
    for _ in 0...10 { addItem() }
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    numberOfItemsInSection section: Int
  ) -> Int {
    return items.count
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath
  ) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: "ItemCell", 
      for: indexPath
    )

    cell.contentView
      .backgroundColor = items[indexPath.item].color

    return cell
  }  
}

Next, we'll embed it in a UINavigationController, and add a UIBarButtonItem to add new items.

We'll Control+Drag the bar button item into our ViewController.swift as a new add: action. We'll wire up it like so:

@IBAction func add(_ sender: UIBarButtonItem) {
  addItem()

  let indexPath = IndexPath(
    item: self.items.count - 1, 
    section: 0
  )

  collectionView?.performBatchUpdates({
    self.collectionView?.insertItems(at: [indexPath])
  }, completion: nil)
}

With this, we have a nice mostly-"boilerplate" setup. Just some square, randomly-colored, cells that fade in when they're inserted:

Not bad. Now let's customize the behavior of those cells as they're being inserted. For this, we'll need our own custom UICollectionViewLayout subclass.

Specifically we'll subclass UICollectionViewFlowLayout and override a few functions to customize the behavior of inserted cells.

class CustomFlowLayout : UICollectionViewFlowLayout {
  var insertingIndexPaths = [IndexPath]()
}

We've added a property to keep track of which index paths are being inserted during each batch update of the collection view.

To populate our property we'll need to override a couple of functions:

override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
  super.prepare(forCollectionViewUpdates: updateItems)

  insertingIndexPaths.removeAll()

  for update in updateItems {
    if let indexPath = update.indexPathAfterUpdate,
                       update.updateAction == .insert {
      insertingIndexPaths.append(indexPath)
    }
  }
}

override func finalizeCollectionViewUpdates() {
  super.finalizeCollectionViewUpdates()

  insertingIndexPaths.removeAll()
}

Nice. Nothing fancy here, we're just collecting the inserted index paths at the beginning of each update, then clearing them out at the end.

The real magic happens when we override one last function:

override func initialLayoutAttributesForAppearingItem(
  at itemIndexPath: IndexPath
) -> UICollectionViewLayoutAttributes? {
  let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)

  if insertingIndexPaths.contains(itemIndexPath) {
    attributes?.alpha = 0.0
    attributes?.transform = CGAffineTransform(
      scaleX: 0.1, 
      y: 0.1
    )
  }

  return attributes
}

What we've done here is to grab the layout attributes our collection view was going to use for the newly inserted item, and modify them slightly, before passing them along back to the system.

Here we've added a transform to scale the item down when it is first added, this will give us a nice "zoom up" effect as each item is added:

That looks neat, but let's try for something even funkier. We can change our transform line to:

attributes?.transform = CGAffineTransform(
  translationX: 0, 
  y: 500.0
)

With this one change, we can achieve an entirely different effect:

Neat!

Download the Xcode project we built in this Bite right here.

That's all for today. Have a specific UICollectionView question you'd like answered? Send it along!.

Animation plays a key role in how we understand the user interfaces in the software we use. This role expands itself when animations are driven directly from a user's gestures or interactions with the interface. Today we'll look at a new framework that can help us create these types of experiences without breaking a sweat. Let's dive in.

It's called Interpolate and it's by Roy Marmelstein.

As the project's README puts it: "all animation is the interpolation of values over time."

Interpolate helps us describe the relationships that we want to exist between a user's gesture and the interpolated values that should result for the properties of our views. Let's try it by animating a color.

let colorChange = Interpolate(
  from: UIColor.whiteColor(),
  to: UIColor.redColor(),
  apply: { [weak self] (result) in
    if let color = result as? UIColor {
      self?.view.backgroundColor = color
    }
  }
)

The Interpolate type holds some configuration, next we'll want to wire it up to a gesture:

func handlePan(recognizer: UIPanGestureRecognizer) {
  let translation = recognizer.translationInView(self.view)
  let translatedCenterY = view.center.y + translation.y
  let progress = translatedCenterY / self.view.bounds.size.height

  colorChange.progress = progress
}

Since Pan GRs report every step of their progress as a simple float (from 0.0 - 1.0), we can simply set that progress percentage value directly on the Interpolate object.

There's tons more too, Interpolate supports easing functions, and works on all sorts of foundation types (points, rects, colors, etc.).

More info about Interpolate can be found at git.io/interpolate

Creating good-looking animations can be a lot of work. We could spend our whole day learning about curves and scaling, and transforms, and oh boy...

Instead, what if we could simply apply some popular animations to our UI elements with almost no effort at all? Today we'll check out a library called Spring that allows us to do just that.

Spring supplies a ton of common animations like shake, pop, squeeze, wobble, etc.

If we're using Interface Builder, we actually don't even need any code at all. We just use one of Spring's UIView-subclasses, then we supply our animation by name.

In code, it's equally simple:

layer.animation = "shake"
layer.animate()

These all work great out of the box, but we can still customize the curve or individual properties if needed. Neat!

More info about Spring can be found at git.io/spring

Topics

#193: UIView Transition Basics πŸ›

Topics

Most iOS developers have used the fantastic UIView animateWithDuration family of functions. But there's another, slightly-lesser known static function on UIView that can help us transition like a pro. Let's check it out:

The function we'll be trying is transitionWithView. At first glance you'll see it's takes the same duration, options, and closures as its more-popular sister function.

However, instead of applying our changes over time, this function will (conceptually) take a snapshot of our view before and after the work in the animations** closure** is performed, then visually transition from the first snapshot to the second.

func flip() {
  flipped = !flipped

  UIView.transitionWithView(
    button,
    duration: 0.3,
    options: .TransitionFlipFromTop,
    animations: {
      self.button.setTitle(self.flipped ? "πŸ‘ŽπŸ»" : "πŸ‘πŸ»", forState: .Normal)
    },
    completion: nil
  )
}

The type of transition depends on the animation option we pass in. We can do everything from simple cross-dissolves (fades), to fancy 3D flips. Super handy for simple transitions.

Download a sample project at j.mp/bite193. In it, we flip a thumbs up emoji using this incredibly simple technique.

Animation is one of the greatest parts about building (and using) iOS apps. The APIs however, can feel a bit scattered. UIView's animation functions are wonderful, but some animations require using Core Animation directly.

When they do, things get progressively more complex depending on if we need to run multiple animations in succession, or just run code after an animation completes.

Today we'll look at a great library from Marin Todorov called EasyAnimation that improves on all of this. Let's dive in:

EasyAnimation makes animating CALayers that normally would require CABasicAnimation (or one of its siblings) work with the standard UIView.animateWithDuration functions:

UIView.animateWithDuration(0.3, animations: {
  self.view.layer.position.y = 64.0
})

Under the hood, EasyAnimation does all the heavy lifting of translating our animations back into CAAnimation code and handling all of the implementation details for us. Neat!

Normally, if we wanted to run code after one the animations on a CALayer finished, we'd need to wire up an animation delegate, implement the callback functions, make sure to clean up after ourselves, etc.

With EasyAnimation though, we're able to just use the normal completion closure.

UIView.animateWithDuration(
  0.3,
  delay: 0.1,
  options: [.BeginFromCurrentState],
  animations: {
  self.view.layer.borderWidth = 2.0
  self.view.layer.cornerRadius = 12.0
}, completion: { finished in
  self.tableView.reloadData()
})

Last but certainly not least, EasyAnimation makes "chaining" multiple animations together (running one after another) extremely convenient. It also supports cancelling the chain, repeating, delays and more:

let chain = UIView.animateAndChainWithDuration(0.3, animations: {
  self.avatarView.center = headerView.center
}).animateWithDuration(0.2, animations: {
  self.headerView.alpha = 1.0
})

More info about EasyAnimation can be found at git.io/easyanimation

Topics

#82: Keyboard Notifications πŸ””

Topics

iOS's software keyboard has evolved quite a bit over the years. Just last week we saw a new variant of it on the new iPad Pro. The user can also connect a hardware keyboard at anytime, hiding the software one. It's important, now more than ever, that we not make any assumptions about how and when the keyboard will transition on and off the screen.

It'd be great if the system could tell us all the details of how and where it's about to animate. Then we could use that info to move our own controls and views right alongside it. For this we can listen for some keyboard notifications_, and react appropriately. Let's take a look.

We'll start by registering for two notifications in viewDidLoad:

NSNotificationCenter.defaultCenter().addObserver(self,
  selector: "keyboardWillShowOrHide:", name: UIKeyboardWillShowNotification, object: nil)

NSNotificationCenter.defaultCenter().addObserver(self,
  selector: "keyboardWillShowOrHide:", name: UIKeyboardWillHideNotification, object: nil)

Fun fact: Starting in iOS 9, we don't need to unregister these in deinit, the system now does it for us! πŸŽ‰

Next, we'll grab all the necessary values out of the notification's userInfo, and use them to animate our own views exactly alongside the keyboard as it slides on or off the screen:

func keyboardWillShowOrHide(notification: NSNotification) {
  let i = notification.userInfo!
  let start = view.convertRect(i[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue(), fromView: view.window)
  let end = view.convertRect(i[UIKeyboardFrameEndUserInfoKey]!.CGRectValue(), fromView: view.window)

  bottomConstraint.constant -= (end.origin.y - start.origin.y)
  view.setNeedsUpdateConstraints()

  let duration = i[UIKeyboardAnimationDurationUserInfoKey]!.doubleValue

  UIView.animateWithDuration(duration, delay: 0, options: .BeginFromCurrentState, animations: {
    self.view.layoutIfNeeded()
  }, completion: nil)
}

We grab the start and end frames, convert them to our view controller's view's coordinate space, and use the difference to move a constraint. Then we animate the constraint like we covered Bite #9.

Animating between two sets of Auto Layout constraints is quite simple.

All you have to do is update your installed/configured constraints and then call layoutIfNeeded inside of a UIView.animateWith* closure.



class DoorsViewController: UIViewController {
    var open: Bool = false {
        didSet { transition() }
    }

    func transition() {
        self.view.layoutIfNeeded() // force layout before animating

        UIView.animateWithDuration(0.4) {
            // change constraints inside animation block
            self.updateConstraintsForDoors()

            // force layout inside animation block
            self.view.layoutIfNeeded()
        }
    }

    func updateConstraintsForDoors() {
        leftDoorHorizontalConstraint.constant = open ? -16 : -leftDoorView.bounds.size.width
        rightDoorHorizontalConstraint.constant = open ? -16 : -rightDoorView.bounds.size.width
    }
}

Download sample project