Code We Trust

Flow
The Startup
Published in
13 min readNov 7, 2020

Flow’s code export is excellent, and for good reason.

First, we’ve been producing award-winning interactive media, installations, and cutting edge interfaces for more than a decade. Flow is the culmination of all of those years of practice and of immersing ourselves in art, tech, and design. We’ve used pretty much every piece of software you can think of, and draw on all our experience to produce Flow. If you want to dive into our background, please check out The Roots of Flow.

Second, we’re experts at writing animation software. Our roots in this started way, way, waaaaay, back with Processing (Java), OpenFrameworks (C++), and transitioned to Objective-C and every version of Swift and Core Animation along the way. Then we jumped to the web where we’re confident in saying “we know our shit” when it comes to the most advanced, native approach for creating animations in the browser: the Web Animations API. Along the way we’ve learned a lot of tricks, and all of that experience is manifest in Flow’s code export. We’ve published a high-level article of our thoughts on Control, Craftsmanship and Code Export that is definitely worth a read.

If you’re reading this article because you’re looking for proof that Flow lives up to its claim, that it ships Production-Ready software, then you’re in for a treat.

Preface: A Swift Context

Flow can generate many different kinds of media files and software, which makes it difficult to show a complete and comprehensive account of every design and architectural decision we made in the production of its code export capability. However, the decision-making and philosophy behind our approach to designing, architecting, and producing exported code is consistent across all formats. So, while this article will heavily focus on Swift code, Core Animation, Xcode and iOS as the basis for exploring patterns… our approach applies to all other exports.

Defining Animation

An animation can be broken down into two main categories: style and timing. By style, I mean the layout, colors, and visual form of the elements that are a part of the animation. By timing, I mean the pacing of changes to the visual style over time.

Style and layout v. Changes Over Time

From a software point of view, these two elements can be understood as (visual) properties and timing functions.

The Syntax of Motion

In the world of iOS, the technology for rendering, composing and animating visual elements is called Core Animation — in the browser the Web Animations API is very similar. Core Animation is a powerful framework that allows a developer to configure parameters and trigger animations to occur. Two of its main strengths are that it basically hands off the compositing of animations to graphics hardware — you don’t have to worry about the in-between moments of your animations — and that it actually defines a nearly universal model for representing animation as software.

Layers

When you look at a mobile app you might see tweets, or music videos, or controls to play your favourite podcast. However, what you’re actually looking at are a complex set of layers that, when overlaid upon one another, make up the entirety of an apps interface. On iOS there are two types of layers: image layers and shape layers. These layers have properties you can set, many of which are visual — shadows, paths, images, borders, etc. — and are defined by values which are often numerical.

Check out all the layers in this one view!

Here’s an example of setting a property on a layer.

ourLayer.opacity = 0.5 //set its value to 50% opacity

Most, but not all, visual properties can be animated. Here a complete list of animatable properties for iOS.

Animations

The simplest form for iOS is called an implicit animation. The default behaviour on iOS is that if a value changes, it animates over a 0.25s period.

ourLayer.position = newPosition //move an object

For a bit more control than this, the technique is to create an animation object. The animation above, when created as an object, looks like this:

let anim = CABasicAnimation(keyPath: "position") 
anim.fromValue = ourLayer.position
anim.toValue = newPosition
anim.duration = 1.0 ourLayer.add(anim, forKey: anim.keyPath)

The key difference here, is that when you create an animation like this you can specify parameters for the animation itself. This animation now takes 1.0s to complete its transition to move from one position to another.

Chaining Animations

Thinking about animations in this way becomes increasingly difficult when you want something more advanced to happen. Imagine you want an element to fade in and fade out. Seems simple, right? Well that animation effect actually needs to be broken down into two opacity animations.

let animIn = CABasicAnimation(keyPath: "strokeWidth")
animIn.fromValue = 0
animIn.toValue = 60
animIn.duration = 1.0
ourLayer.add(animIn, forKey: anim.keypath)
animIn.delegate = self
let animOut = CABasicAnimation(keyPath: "strokeWidth")
animOut.fromValue = 60
animOut.toValue = 0
animOut.duration = 1.0
animOut.isRemovedOnCompletion = false
animOut.fillMode = .forwards
ourLayer.add(animOut, forKey: anim.keypath)

This is actually two animations, the first from 0 ➔ 60 and the second from 60 ➔ 0.

Notice how things are getting complicated? You need to know to remove animations (or not), you need to know how to set the fillMode, and even set the delegate for the first animation.

Two animations for the stroke width: 0 ➔ 60 and 60 ➔ 0

The explicit animation approach (above) gets unruly very, very fast. The solution to this is to use keyframe animations, which let you set values and timing functions.

Here is the same animation as above, using keyframes:

let anim = CAKeyframeAnimation(keyPath: "opacity")
anim.values = [0, 60, 0]
anim.keyTimes = [0, 0.5, 1]
anim.duration = 2.0
anim.fillMode = .forwards
anim.isRemovedOnCompletion = false
ourLayer.add(anim, forKey: "opacity")

Easier. Cleaner. But, still with a level of complexity. You need to think of your animation timing in terms of values from 0…1 and how those translate to the overall duration of the animation.

Easing

Let’s have a look at easing. Here’s an animation that animates the movement of a layer from the bottom to the top of its frame and back. In iOS, the default easing curve is Ease In Out.

let anim = CAKeyframeAnimation(keyPath: "position.y")
anim.values = [100, 0, 100]
anim.keyTimes = [0, 0.5, 1]
anim.duration = 2.0
anim.fillMode = .forwards
anim.isRemovedOnCompletion = false
ourLayer.add(anim, forKey: "position.y")

Let’s say you want to use two different timing functions for each part of the animation. You would do something like this:

anim.timingFunctions = [CAMediaTimingFunction(name: .easeIn), CAMediaTimingFunction(name: .easeOut)]

Great, but the problem with this is that most animation engines have only a basic set of predefined easing curves. In iOS, these are easeIn, easeInOut, easeOut and linear. What if you wanted something custom, say from easings.net. Well, you'd have to roll your own timing function like so:

let cubic = CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19)
let back = CAMediaTimingFunction(controlPoints: 0.68, -0.55, 0.265, 1.55)
let custom = CAMediaTimingFunction(controlPoints: 1, 0, 0.66, 1)

Here is what these timing functions look like:

Other Animation Things

Other things to consider with animations are:

  • paths
  • gradients
  • round corners
  • groups
  • snapping from one value to another
  • multiple animations for multiple properties on the same layer
  • reversing complex animations
  • reversing complex animations with custom easing curves
  • managing animation states
  • adding controls (e.g. ability for a user to pause an animation)
  • rotations and transforms

This is just a small, small list of things to thing about when writing out animation code, and one of the main reasons why getting animation into your apps is difficult, time consuming and expensive.

Flexible, Robust and Easy

The best way to write animation code is to be clean clean, modern and consistent. But, that’s just one part of how we approach exporting production-ready animations. The other critical component is making it easy to interact with and control the software.

A View & A Timeline

Every animation, no matter how complex, is broken down into two classes of files: a view and a timeline. This dichotomy makes it easy to identify and modify the layout of the animation, and separates the animation code into its own class that can be hot-swapped if needed.

AnimationView

An animation’s view is a subclass of UIView which means it works exactly like a native view on iOS. Each animation is given a unique name when it is exported, so you'll see things like this:

  • DialedView
  • GradientRingView
  • GriddyView

A view gets created just like a normal UIView:

let view = DialedView(frame: frame)

After initializing, the view runs its setup and creates each layer in your animation:

private func createViews() {
CATransaction.suppressAnimations {
createDial()
createMask()
createBars()
//etc...
}
}

See that suppressAnimations block? That's an incredibly handy trick we created after years of mastering Core Animation.

As you can see, there will be a unique method constructing each layer:

private func createDial() {
dial = ShapeView(frame: CGRect(x: 18.5, y: 18.5, width: 32, height: 32))
dial.backgroundColor = UIColor.clear
dial.layer.position = CGPoint(x: 18.5, y: 18.5)
//etc...
}

As an added touch, we have a great method for scaling your view:

public func scale(to size: CGSize) {
let x = size.width / Defaults.size.width
let y = size.height / Defaults.size.height
transform = CGAffineTransform(scaleX: x, y: y)
}

This scale method makes it really easy for a developer to quickly resize the animation based on layout constraints.

AnimationTimeline

The timeline is a unique class that encapsulates pure Core Animation code. Each timeline is given a unique name when it is exported, so you’ll see things like this:

  • DialedTimeline
  • GradientRingTimeline
  • GriddyTimeline

The really cool thing is that you can pass a view into a timeline. This means that the timeline can be reused on multiple views — very handy for buttons that should all have the same animation.

DialedTimeline(view: dialed, duration: 2)

If you want your animation to repeat and/or reverse, there are options for that:

DialedTimeline(view: dialed, duration: 2, repeatCount: 10, autoreverses: true)

The timeline contains playback controls:

  • timeline.play()
  • timeline.pause()
  • timeline.stop()
  • timeline.reset()
  • timeline.offset(to: time)

There are some other fancy controls, but you get the idea… things are straightforward.

The timeline will have a unique method that generates a keyframe animation for any animated layers in your file:

let transform_rotation_z_dial: CAKeyframeAnimation = {
let anim = CAKeyframeAnimation()
anim.keyPath = "transform.rotation.z"
//etc...
return anim
}()

Architected for Animation

Every project exported from Flow has a carefully crafted structure that employs a forward-thinking approach to integrating animations into your products.

Common Files — Not a Library

As we’ve mentioned above, animation code can be very unruly. Over the years, as we’ve come to master Core Animation, we’ve created a few tricks along the way that help make making animations easier.

These “tricks” are encapsulated in a set of files we call FlowCommon. There are two versions of this, one for iOS and the other for Web, both of which have their own CocoaPods. Most of the files in this collection are very minimal, adding a convenience function or two.

For example, the CATransaction+Extension.swift file has only this method in it:

extension CATransaction {
static public func suppressAnimations(actions: () -> Void) {
begin()
setAnimationDuration(0)
actions()
commit()
}
}

An incredibly handy method for turning off the default 0.25s animation during setup.

FlowCommon is not a library. The majority of files have less than 20 lines of actual code in them, so these are very easy for a developer to review and approve.

Interface Files Made Easy

Our Xcode projects have a Main.storyboard file that contains a view. For our basic iOS export, that view’s class is the unique AnimationView for your animation. The elegance of this approach means you can easily change the class of a view into an animation view via Interface Builder.

For more custom exports like our iOS Custom Activity Indicator, the animation view may be embedded dynamically into a native view.

private func setup() {
view = createView()
addSubview(view)
}
override func createView() -> UIView {
return dialed
}
private lazy var dialed: DialedView = {
let view = DialedView(frame: .zero)
view.scale(to: frame.size)
return startView
}()

This approach is consistent with being able to add a native component to your Interface Builder file, then simply change it’s class to the custom one exported from Flow.

Everything exported from Flow is a native component for the platform. In the case of iOS, an animation view is a UIView and an animated spinner is actually a UIActivityIndicatorView. In each case, the animation is "slipped" in to the native element.

Very Nice.

And if you look very closely, you’ll see that every project exported from Flow shows you how to add a loading animation to your app!

Trusting Generated Code

Animations can be big or small. Launch and onboarding animations are great places to put in a lot of effort, nuance and beautiful added touches. In contrast, micro-animations can create real moments of delight when users interact with otherwise mundane elements of an interface, like buttons, sliders, and loading indicators.

From a software perspective, there very little difference between the simple and complex animations. In the end, they’re just a collection of keyframe animations being triggered at the right points in time.

Animation Code is Boring

Animation code is actually a very mundane thing to write. You create a layer, set up its initial properties, you create some animations and apply them to the layer when things are supposed to kick off. You do this over, and over, and over, and over again for every property and every layer. If you do things properly, then writing animation code really just a whole bucketful of lather, rinse, repeat.

  1. choose a property
  2. set its value(s)
  3. set its easing(s)
  4. choose a duration
  5. trigger it

These 5 main steps are just repeated for every layer, and every animation throughout your apps.

It’s mundane.

It’s boring.

It’s trivial and…

That’s a very good thing.

Generated Code is Trustworthy Code

When there is little variability in the kind of code that you need to produce, then using generated code is by far the best way to get things done. If you know the patterns you need to produce, you can easily set up a generator to do the heavy lifting for you. That generator will follow the rules you define, systematically, accurately, every single time.

If you trust the rules that govern the generator, then it follows that the code being generated is trustworthy.

50 Activity Indicators

Until now we’ve mostly been looking at animations from a high-level perspective. So, I’d like to end this article by looking at real-world examples of code generated from Flow.

We recently published an open-source project with 50 custom iOS Spinners, that express a wide variety of animation techniques. Here’s a few examples with some code excerpts to show how things work.

Dialed

This animation is really interesting. The effect looks really complicated, but the technique is quite simple.

We call the whole element dial , which is essentially a layer with a doughnut as a mask.

let dialMask = CAShapeLayer()
dialMask.path = CGPathCreateWithSVGString("M16,0c-8.837,0,-16,7.163,-16,16 0,8.837,7.163,16,16,16 8.837,0,16,-7.163,16,-16 0,-8.837,-7.163,-16,-16,-16zM16,8c4.418,0,8,3.582,8,8 0,4.418,-3.582,8,-8,8 -4.418,0,-8,-3.582,-8,-8 0,-4.418,3.582,-8,8,-8zM16,8")!
dial.layer.mask = dialMask

Inside that layer is a group of 5 bars of color.

dial.addSubview(bars)

The bars group moves through the mask in a downward angle.

anim.keyPath = "position.x" 
anim.values = [-34, 67]
anim.keyPath = "position.y"
anim.values = [-34, 67]

The top layer rotates.

anim.keyPath = "transform.rotation.z" 
anim.values = [0, 6.28319] //aka 2𝜋, aka 360°
anim.keyTimes = [0, 1]

Griddy

Here’s an interesting visual effect that you get by running the same animation 16 times, on 16 identical squares in a grid, with their start times offset by a little bit.

The trick with this one isn’t to animate the size of the squares, rather the line widths of their edges!

anim.keyPath = "lineWidth"
anim.values = [4, 4, 0, 0, 4, 4]
anim.keyTimes = [0, 0.205479, 0.547945, 0.719178, 0.993151, 1]
anim.timingFunctions = [.linear, CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19), .easeInEaseOut, CAMediaTimingFunction(controlPoints: 0.215, 0.61, 0.355, 1), CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19)]

As you can also see, we’re using some custom easing curves as well — specifically cubic-in and cubic-out.

Gradient Ring

You can simulate a ring with gradient color by using masks in a sneaky way.

First, create a gradient.

let startPoint = CGPoint(x: 0.5, y: -0.0450368)
let endPoint = CGPoint(x: 0.5, y: 1)
let colors = [UIColor(red: 0.463, green: 0.275, blue: 0.988, alpha: 1).cgColor, UIColor(red: 0.314, green: 0.888, blue: 0.76, alpha: 1).cgColor]

Then apply a mask:

gradient.layer.mask = foregroundCircle.layer

To make the ring, the foreground circle has a clear color:

foregroundCircle.shapeLayer.fillColor = nil

Then, animate the path of the foreground circle:

anim.keyPath = "strokeStart"
anim.values = [0.75, 0.7, 0, 0]
anim.keyTimes = [0, 0.17, 0.55, 1]
anim.timingFunctions = [.easeInEaseOut, .easeInEaseOut, .easeInEaseOut]
anim.keyPath = "strokeEnd"
anim.values = [1.001, 1.001, 0.25, 0.25]
anim.keyTimes = [0, 0.32, 0.55, 1]
anim.timingFunctions = [.linear, .easeInEaseOut, .easeInEaseOut]

Finesse == Flow

The animations in this article were all made with Flow, and while it is very possible to make all of these by hand, the speed at which I was able to create these and level of detail in the software would be impossible to achieve without Flow.

Take the keyTimes from the Griddy example:

anim.keyTimes = [0, 0.205479, 0.547945, 0.719178, 0.993151, 1]

The smooth poppiness of the animation comes from my playing around with the distribution of keyvalues while watching the animation on loop. Only when I was happy with the outcome did I export the animation to code. If I coded this by hand there’s no way I’d ever be able to consider these values.

Instead, I might only be able to think through them in fairly “round” numbers, which would strip away the finesse that makes the animation really nice. On my own, I would come up with:

anim.keyTimes = [0, 0.25, 0.5, 0.75, 1, 1]

But here’s the thing… I would never spend time hand-coding the Griddy animation. My experience tells me that it would to take way too long for something so small as a loader.

If someone brought me this animation, made with Flow, I’d drop it in my project in a heartbeat.

“Why?”

Because I know that the code-generator behind Flow is reliable, and the code it exports is a reflection of decades of experience and a great deal of craftsmanship.

If there’s anything you can take away from this article, I hope it’s this:

I trust the code that Flow ships, and so can you.

Originally published at https://createwithflow.com on November 7, 2020.

--

--

Flow
The Startup

A new class of UI Animation software. Import from Sketch. Animate in Flow. Export production-ready code.