Using SpriteKit to create animations in Swift
Basics article available: AnimationsAnimations are a great way to explain the functionality of our apps through motion, and to delight our users. Adding animations in the right places can really make a UI look a lot more polished and nice.
But creating great animations require quite a lot of tweaking and iterations, since we often need to experiment with different animation steps, durations and curves to get things to feel just right.
That's why it's so important to build animations using tools that enable us to easily change things and tweak parameters. This week, let's take a look at how SpriteKit can act as such a tool for certain kinds of animations.
The basics
SpriteKit is Apple’s built-in framework (since iOS 7) for 2D game development. So while it’s primarily targeted towards making games, it’s also a really nice tool for any kind of 2D drawing and animations. In fact, at WWDC 2017, Apple revealed that they are actually using SpriteKit to build the UI for the memory debugger in Xcode.
For animations, SpriteKit can be really useful when building more complex, self-contained scenes. For example, when you are creating some form of full screen loading animation, an illustration as part of your onboarding flow, or anything else that includes multiple animation steps and doesn’t involve directly animating views and UI controls.
As an example, we’re going to build a loading animation containing 4 emojis, that's going to look like this:
Setting the scene
All SpriteKit content is presented in a scene, managed by an instance of the SKScene
class. Content is then defined using a node based system, that enables you to create hierarchies, just like when using UIView
s or CALayer
s.
You create nodes using various subclasses of SKNode
, for example SKSpriteNode
for sprite (image) based content, or SKLabelNode
for textual content.
Finally, you use actions (represented by the SKAction
class) to make your nodes perform various animations (such as moving, scaling, rotating, etc) in your scene.
Getting started
Let’s start by creating an SKScene
as a container for our animation. We give it a square size taken from the minimum dimension of a view controller’s view, and set a white background color:
extension AnimationViewController {
func makeScene() -> SKScene {
let minimumDimension = min(view.frame.width, view.frame.height)
let size = CGSize(width: minimumDimension, height: minimumDimension)
let scene = SKScene(size: size)
scene.backgroundColor = .white
return scene
}
}
You present an SKScene
using an SKView
(which is a UIView
subclass on iOS). We’ll add such a view to our view controller, and set its size and center point. Finally, we tell it to present our scene, like this:
class AnimationViewController: UIViewController {
private lazy var animationView = SKView()
override func loadView() {
super.loadView()
view.addSubview(animationView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Make sure we don't recreate the scene when the view re-appears
guard animationView.scene == nil else {
return
}
let scene = makeScene()
animationView.frame.size = scene.size
animationView.presentScene(scene)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
animationView.center.x = view.bounds.midX
animationView.center.y = view.bounds.midY
}
}
Adding nodes
So we now have a scene to render in, and a view that will present it in our view controller. Let’s start adding some content. If you were rendering images and animations using keyframes, you’d want to use SKSpriteNode
for rendering. But in this example we’ll stick with some simple emojis, so we’re going to use SKLabelNode
(which is basically the SpriteKit equivalent of UILabel
).
Let’s start by creating an extension on SKLabelNode
, which lets us render an emoji:
extension SKLabelNode {
func renderEmoji(_ emoji: Character) {
fontSize = 50
text = String(emoji)
// This enables us to move the label using its center point
verticalAlignmentMode = .center
horizontalAlignmentMode = .center
}
}
We’ll then create another extension method on our view controller to add all emoji our scene (which we’ll call from makeScene()
):
extension AnimationViewController {
func addEmoji(to scene: SKScene) {
let allEmoji: [Character] = ["🌯", "🌮", "🍔", "🍕"]
let distance = floor(scene.size.width / 4)
for (index, emoji) in allEmoji.enumerated() {
let node = SKLabelNode()
node.renderEmoji(emoji)
node.position.y = floor(scene.size.height / 2)
node.position.x = distance * (CGFloat(index) + 0.5)
scene.addChild(node)
}
}
}
Let’s animate!
With all the setup out of the way, let’s get to the fun part - actually creating our animation. We’ll perform the animation by telling each emoji node to scale up and then back down, with a slight delay depending on the index of the node:
func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
// Offset each node with a slight delay depending on the index
let delayAction = SKAction.wait(forDuration: TimeInterval(index) * 0.2)
// Scale up and then back down
let scaleUpAction = SKAction.scale(to: 1.5, duration: 0.3)
let scaleDownAction = SKAction.scale(to: 1, duration: 0.3)
// Wait for 2 seconds before repeating the action
let waitAction = SKAction.wait(forDuration: 2)
// Form a sequence with the scale actions, as well as the wait action
let scaleActionSequence = SKAction.sequence([scaleUpAction, scaleDownAction, waitAction])
// Form a repeat action with the sequence
let repeatAction = SKAction.repeatForever(scaleActionSequence)
// Combine the delay and the repeat actions into another sequence
let actionSequence = SKAction.sequence([delayAction, repeatAction])
// Run the action
node.run(actionSequence)
}
}
The above code works, but is quite hard to read and reason about, especially if we were to take away the comments. But the good news is that we can easily fix it! Thanks to Swift’s awesome dot notation syntax, we can heavily reduce the verbosity of our code, and get rid of all the temporary let
assignments:
extension AnimationViewController {
func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
node.run(.sequence([
.wait(forDuration: TimeInterval(index) * 0.2),
.repeatForever(.sequence([
.scale(to: 1.5, duration: 0.3),
.scale(to: 1, duration: 0.3),
.wait(forDuration: 2)
]))
]))
}
}
}
We can now start our animation by calling the above method with all of our scene's nodes in makeScene()
:
animateNodes(scene.children)
With a twist
Now that we have easy to read (and very declarative) animation code, we can start to play around with it. Let’s say we wanted to add a little twist (literally!) to our animation, by making our emoji rotate 360 degrees at the same time as they’re scaling up and down.
All we have to do to make this happen is to combine the scaling actions and a rotation action into a group, like this:
extension AnimationViewController {
func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
node.run(.sequence([
.wait(forDuration: TimeInterval(index) * 0.2),
.repeatForever(.sequence([
// A group of actions get performed simultaneously
.group([
.sequence([
.scale(to: 1.5, duration: 0.3),
.scale(to: 1, duration: 0.3)
]),
// Rotate by 360 degrees (pi * 2 in radians)
.rotate(byAngle: .pi * 2, duration: 0.6)
]),
.wait(forDuration: 2)
]))
]))
}
}
}
Which will give us the following result:
Conclusion
As you can see in the example above, using SpriteKit for animations enables us to write very declarative animation code that is easy to change, extend and experiment with. It’s definitely not a silver bullet for animations, since we have no way of animating any form of UIView
s this way, but for self-contained scene based animations I think it’s a super nice tool.
There’s of course a lot more to SpriteKit than what I was able to cover in this post, so please let me know if you’d like me to do another post that goes more in-depth on SpriteKit and more of its capabilities (like physics, interactions, etc).
What do you think? Will you try using SpriteKit the next time you need to build a more custom animation, like the one in this post? Let me know, along with any other questions or feedback that you might have, on Twitter @johnsundell.
Thanks for reading! 🚀