SDK Examples
Spinner
In this tutorial we will build a spinner and learn how to manipulate core animations from Squall in Xcode.
Setting up the animation
After Effects
Our animation in After Effects is a shape layer parented to a null object.

We have animated one turn of the spinner in After Effects and will use Xcode to loop the animation.

We want the null to rotate independently and we also want the colors to shift independently. We do, however, want the trim path end and the z rotation of the layer to sync up and repeat in the same rythm. So we are animating the end of a trim path from 85% to 1% to 85% and the rotation from 0 to 0 to 360°. The extra zero keyframe in the beginning ensures that the animations are of the exact same length and will therefore repeat together.

Xcode
We will build a class called Spinner that inherits from UIView. It contains our spinner animation (spinner.sqa) from After Effects as a sublayer.
Setup
var spinnerAnimation : SLCoreAnimation!
let r = SLReader()
var animationInfo : SLAnimationInformation?
do {
    animationInfo = try r.parseFileFromBundle("spinner.sqa")
} catch {
    print("error \(error)")
}
This code sets up a SLReader which reads our sqa file and spits out a SLAnimationInformation object.

if animationInfo != nil {
    let a = SLCoreAnimation.init()
    a.buildDelegate = self
    a.buildWithInformation(animationInfo!)
    a.play()
    self.frame = a.frame
    self.layer.addSublayer(a)
}
For the spinner we are using a SLCoreAnimation rather than SLSquallAnimation because they are more resilient to stalls on the main thread and their interface allows for independent property looping.

Before we build the animation we set the our spinner class as the build delegate for this SLCoreAnimation. Our spinner adheres to the SLCoreAnimationBuildDelegate protocol.

After we build the animation we set the frame of the containing layer to the frame of the animation. The frame of the animation is determined by the composition size in After Effects.

Manipulating the build process
The SLCoreAnimationBuildDelegate protocol is where the magic happens and we have access to the underlying animation components.

The Spinner class implements the shouldAddAnimations method. For each animated layer in After Effects the delegate gets a callback with a CAAnimationGroup, a CALayer and a String.

shouldAddAnimations( group: CAAnimationGroup, toLayer layer: CALayer, withName name: String)
Animation
CAAnimationGroup containing all animations Squall wants to add to the layer.
Layer
The CALayer Squall constructed to add all the animations to. This does not necessarily directly correspond to an After Effects layer.
Name
The name of the layer as it appears in your After Effects composition.
To get a sense of what Squall is building try logging the layer name and animation key paths to the console.

func shouldAddAnimations(group: CAAnimationGroup, toLayer layer: CALayer, withName name:String) -> CAAnimationGroup? {
        print("AE layer \(name)")
        for ani : CAKeyframeAnimation in group.animations! as! [CAKeyframeAnimation] {
            print("\tanimating \(ani.keyPath)")
        }
        return group;
    }
Output:

AE Layer: SpinnerPath
	animating Optional("strokeEnd")
	animating Optional("strokeColor")
AE Layer: SpinnerGroup
	animating Optional("transform.rotation.z")
AE Layer: Null
	animating Optional("transform.rotation.z")
AE layers do not directly correspond to CALayers. Paths and vector transform groups, for example, are represented by their own CALayer.

It is always a good idea to log the values the build delegate get passed before trying to manipulate them.


func shouldAddAnimations(group: CAAnimationGroup, toLayer layer: CALayer, withName name:String) -> CAAnimationGroup? {
        switch name {
        case "Null":
                for ani in group.animations! as! [CAKeyframeAnimation] {
                    if ani.keyPath == "transform.rotation.z" {
                        ani.repeatCount = 10000.0
                    }
                }
            
        case "SpinnerPath":
                for ani in group.animations! as! [CAKeyframeAnimation] {
                    if  ani.keyPath == "strokeColor" || ani.keyPath == "strokeEnd" {
                        ani.repeatCount = 10000.0
                    }
                }
        case "SpinnerGroup":
                for ani in group.animations! as! [CAKeyframeAnimation] {
                    if ani.keyPath == "transform.rotation.z" {
                        ani.repeatCount = 10000.0
                    }
                }
        default:
            break
        }
      
        group.duration = 10000.0 
    }

Project Files