Interpolating Animations

TL;DR: Animations can be built by interpolating your types.

We love Swift. And we use Swift not only for the sake of Swift. It also enables us to write our applications in a safer manner. Plus it gives us concepts to empower reusable code parts. Ultimately we can spend more time on thinking about our apps, than on writing code.

Watch out, for the concepts ⚒

So let’s apply some Swift concepts to build a cool feature for the watch.

When we did the first watch app we chose a data driven app. Knowing that the capabilities are constrained and not yet having the hardware was an interesting situation. The watch should display the data in bar diagrams. When you open the app - the diagrams should perform a transition to the new state. Therefore you need to build a list of images with the intermediate states.

The hard facts:

  • You start with empty screen, empty horizontal bar.
  • After fetching data we get 3 BarSegments with different colors and width. The change is animated.
  • Maybe next time you receive 4 BarSegments, where the previous ones have a different width and maybe color. And the change is animated. So far so easy.

To gif you an idea:

Watch

In·ter·po·late 🚥

We start like a california roll - inside-out. The key concept here is a protocol called Interpolation.

protocol Interpolation {
  /// should be associatedtype, but for syntax highlighting =)
  typealias Result
  func interpolate(upTo upTo: Self) -> Double -> Result
}

Every type conforming to this protocol can be interpolated, between a start and an end. Next we wrap it in struct.

struct Interpolate<T: Interpolation> {
  let from: T
  let to: T
  /// keeping the interpolationValue between 0 and 1
  func atFraction(fraction: Double) -> T.Result {
    let p = max(0, min(fraction, 1))
    return from.interpolate(upTo: to)(p)
  }
}

The idea is pretty easy and you should get the whole idea when looking at the following example, which is using custom operators.

// interpolate between 0 and 100 and gimme the value at 0.4
// 0 |~| 100 returns a Interpolate Type
// |~> calls 'atFraction'
let middle = 0 |~| 100 |~> 0.5
// -> 50

Building up from the base types, we can make the BarSegment conform to Interpolation. You will find all implementations on the end in the gist.

struct BarSegment {
  let color: UIColor
  let percent: CGFloat
}

extension BarSegment: Interpolation {
  func interpolate(upTo upTo: BarSegment) -> Double -> BarSegment {
    // interpolate UIColor
    let color = self.color |~| upTo.color
    // interpolate CGFloat
    let percent = self.percent |~| upTo.percent
    // return a closure to get the interpolation for a atPercent value
    return { atPercent in
      BarSegment(color: color |~> atPercent, percent: percent |~> atPercent)
    }
  }
}

Zip like a pro 🤐

In order to get a consistent list of bar segments at the beginning and the end of the transition we need to ensure that both lists have the same number of bar segments.

We zip those lists with a custom generator. It gives us the possibility to fill up the missing elements, without doing it manually.

// super flexible
protocol ImplicitDefault {
  static var implicitDefault: Self { get}
}

// a SequenceType where the containing 2 SequenceTypes
// have a ImplicitDefault Generator Element
struct ZipWithFillUp<T,U where
  T: SequenceType,
  U: SequenceType,
  T.Generator.Element: ImplicitDefault,
  U.Generator.Element: ImplicitDefault
  >: SequenceType {
  let first: T
  let second: U
  
  init(_ first: T, _ second: U) {
    self.first = first
    self.second = second
  }
  
  func generate() -> AnyGenerator<(T.Generator.Element, U.Generator.Element)> {
    var generator1 = first.generate()
    var generator2  = second.generate()
    
    return AnyGenerator {
      let element1 = generator1.next()
      let element2 = generator2.next()
      
      switch (element1, element2) {
      // both sequences contain an element? 
      case let (e1?, e2?):   return (e1, e2)
      // only one contains an element? -> fill up with implicitDefault     
      case let (.None, e2?): return (T.Generator.Element.implicitDefault, e2)
      // only one contains an element? -> fill up with implicitDefault
      case let (e1?, .None): return (e1, U.Generator.Element.implicitDefault)
      // both empty? stop 
      case (.None, .None):   return nil
      }
    }
  }
}

Watch on, for the animation ⌚️

First we need to zip the BarSegments, conforming to ImplicitDefault. Next we map the list of tuples into Interpolations. Then we stride over the values from 1 to 100 and build up the related list of BarSegments. Each one interpolated at this point. All we need is some easy cheesy drawing methods and the actual example is pretty much done:

let before = [
  BarSegment(color: .redColor(),    percent: 10),
  BarSegment(color: .redColor(),    percent: 10),
  BarSegment(color: .orangeColor(), percent: 30),
  BarSegment(color: .greenColor(),  percent: 50)
]

let after = [
  BarSegment(color: .greenColor(), percent: 50),
  BarSegment(color: .redColor(),   percent: 30),
  BarSegment(color: .greenColor(), percent: 20)
]

// align your interpolations
// - 1. get a list of BarSegment Tuples by calling zipWithFillUp
// - 2. apply the |~| operator to get a list of interpolations
let interpolations = before.zipWithFillUp(after).map(|~|)

// function to get the interpolations at a certain point
let barsAtFraction = { value in interpolations |~> value }

// map the bars interpolated for each value between 0 and 1
// flat map straight to your image
let bars = 0.stride(through: 1, by: 0.01)
            .map(barsAtFraction)
            .flatMap(drawBarImage(CGSizeMake(200, 30)))

// lets show it in the playground
let image = UIImage.animatedImageWithImages(bars, duration: 2)
XCPlaygroundPage.currentPage.liveView = UIImageView(image: image)

Here is the playground to try it out.

👻 Feel free to hit me on twitter. @elmkretzer