Sequencing Exercise Animations in Streaks Workout

Streaks Workout is an Apple TV app that helps you get fit. One of the things we wanted to do differently to other fitness apps is how exercise demonstrations are shown.

Rather than displaying live-action video or lengthy text-based explanations, we wanted a simple animation to show how to perform a push-up, crunch or squat (plus more!).

You can see an animation example in the first screenshot on the Streaks Workout web site.

For a crunch, there are two separate images. icon_crunch_0, which is the "up" position, and icon_crunch_1, the down position:

Other exercises use more than two images, so there needs to be a way to specify this.

To manage properties of each exercise type, I use an enum called Exercise, with each exercise represented by a different case:

enum Exercise {
    case Crunch

    func animationNumImages() -> Int {
        switch(self) {
        case .Crunch: return 2
        }
    }
}

For a crunch, we decided the animation should last about two seconds, but since other exercises may take longer to demonstrate, this needed to be specified for each exercise:

enum Exercise {
    case Crunch

    func animationDuration() -> NSTimeInterval {
        switch self {
        case .Crunch: return 2
        }
    }
}

Although this is somewhat subjective, I decided the animation should show the "down" image slightly longer (as in - the rest at the bottom is slightly longer than the amount of time you hold the crunch for).

For a two second animation, I figured this was about 1.2s down and 0.8s up, which represents a 3:2 ratio.

This can be represented in an array with 3 occurrences of the "down" image, and 2 occurrences of the "up" image), or [ 1, 1, 1, 0, 0 ].

The first frame in an animation is the one shown when not animating. For a crunch, the "up" position best represents the exercise, so I wanted that one displayed when the animation is stopped. This changes the array [ 0, 0, 1, 1, 1 ].

The only problem with this is that once the animation starts, it'll play an extra frame of the "up" frame, which will make it seem like the animation hasn't started yet for an extra 0.4s.

Simple fix! Shift one of the "up" frames to the end of the animation: [ 0, 1, 1, 1, 0 ]. When it loops, the sequence will still be the same.

enum Exercise {
    case Crunch

    func animationSequence() -> [Int] {
        switch(self) {
        case .Crunch:
            return [ 0, 1, 1, 1, 0 ]
        }
    }
}

Since the images above are named icon_crunch_0 and icon_crunch_1, we need a way to access the crunch string from code.

To achieve this, the Exercise enum is changed to be of type String, and crunch assigned to the .Crunch case.

enum Exercise: String {
    case Crunch = "crunch"
}

This means the image can be easily retrieved by its numerical index and the rawValue of the enum:

enum Exercise: String {
    case Crunch = "crunch"

    private func animationImageNameAtIndex(idx: Int) -> String {
        return String(format: "icon_%@_%d", self.rawValue, idx)
    }
}

Finally, the animated image needs to be built using the number of images, duration and sequence. UIImage has built-in support for animated images, but to do so we need to build an array of each still image ([UIImage]) to supply to it.

enum Exercise: String {
    case Crunch = "crunch"

    func animatedImage() -> UIImage {
        var frames: [UIImage] = []

        for idx in self.animationSequence() {
            let name = animationImageNameAtIndex(idx)
            
            if let image = UIImage(named: name) {
                frames.append(image)
            }
        }

        let duration = self.animationDuration()
        return UIImage.animatedImageWithImages(frames, duration)
    }
}

As shown above, once we have the [UIImage] array, UIImage.animatedImageWithImages() is called to build the animated image from those frames.

To use this code, you can use it with a UIImageView as follows:

let animatedImage = Exercise.Crunch.animatedImage()

let imageView = UIImageView(image: animatedImage)

imageView.startAnimating()
imageView.stopAnimating()

There are other animation-related controls with UIImageView also, but these are the main ones needed.