How to create simple SwiftUI animations

Animations are an easy way to show users the functionality hidden behind a beautiful wrapper.
Animations are quite an important part of any application because it draws users’ attention. An animation is simply a collection of images that are repeated at high speed, but animations can set your application apart.
There are many types of animations in UIKit. Some take a lot of time and resources and are quite difficult to implement.
Of course, there are a huge number of third-party libraries (pods) for creating animations, but nevertheless native development is much more effective and makes it possible to protect against unforeseen situations (for example, the impossibility of customization or crashes).
SwiftUI provides a powerful encapsulated mechanism for creating animations without using third-party resources.
In this article, we’ll briefly look at the animations in SwiftUI and will create a simple animation as an example.
SwiftUI basics
When using SwiftUI, you describe your user interface declaratively and leave the rendering to the framework.
Each of the views you declare for your UI (text labels, images, shapes, etc.) belongs to the View protocol. The View requires each view structure to feature a property called body. Whenever the state of a view changes, it recounts its body property and generates a new view.
Any time you change your data model, SwiftUI asks each of your views for its current body, which might change according to changes in your latest data model. It then builds the view hierarchy to render on the screen. In a sense, SwiftUI makes "snapshots" triggered by changes in the data model.
SwiftUI keeps track of the state in the previous snapshot and can animate any changes you declare for your views.
General tips for SwiftUI animations
There are two types of animations in SwiftUI: explicit and implicit. Implicit animations are animations that you specify using the .animation() modifier. This method is used on bindings, and it asks SwiftUI to animate any changes that result in the binding’s value being modified. Parameters that are often animatable include size, offset, color, and scale.
When you’re working with а regular state rather than bindings, you can animate changes by wrapping them in a withAnimation() call. This is also called an explicit animation and is specified with a closure. By default, SwiftUI uses fade in and fade out for animating changes.
To disable/hide animations, you should use .animation(nil).
The power of SwiftUI animations is that you don’t need to take care how the views are animated. All you need to do is provide the start and end state. SwiftUI will then figure out the rest. If you understand this concept, you can create various types of animations.
Differences between SwiftUI and UIKit animations
When developing using UIKit, it’s necessary to manually hide and show the elements of the presentation hierarchy. But in SwiftUI, there’s no need to add or remove a loading indicator.
SwiftUI applies comparison algorithms to understand the differences and automatically add, remove, and update the necessary views. By default, SwiftUI uses the standard in/out transition image to show/hide views, but it’s also possible to manually change the transition to any other animation.
SwiftUI doesn’t display the structure of the view using one-to-one mapping. It’s possible to use as many view containers as you like. But in the end, SwiftUI displays only those views that make sense for rendering. This means you can extract view container logic into small representations, then compose and reuse these views in the application. But don’t worry, as the application’s performance will not suffer in this case.
Real-world example
There are many options for animating your application. Let’s consider one example.
A very common animation case involves a list with rows. By clicking on one of the rows, we’ll move to the details of the item listed in that row. Some information is presented on the details screen itself, usually with text and a picture. In order not to clutter and visually overload the layout, we make the image small. When you click on this image, it will be enlarged using an animation (and it will shrink similarly).
As an example, we’ll use a hypothetical application with a list of French impressionists, their biographies, and one representative work for each. The layout of the main screen looks like this:

First, we’ll create a list of artists. A list is a container that presents rows of data arranged in a single column. To create a list, first add a new structure that will represent the data model.
import SwiftUI struct PainterView: Identifiable { var id = UUID() var image : String var name : String }
This structure conforms to the Identifiable protocol, which will allow views to uniquely identify the items in the structure. The only requirement for implementing the Identifiable protocol is that there must be an id variable. Here the UUID() method is used to give the id a unique number.
In the ContentView structure, add a painterData property that’s an array of PainterView data.
let painterData: [PainterView] = [ PainterView(image: "red_vineyards_near_arles", name: "Vincent van Gogh"), PainterView(image: "boulevard_montmartre_at_night", name: "Camille Pissarro"), PainterView(image: "claude_monet_walk", name: "Claude Monet"), PainterView(image: "flooding_at_port_marly", name: "Alfred Sisley"), PainterView(image: "my_garden", name: "Édouard Manet"), PainterView(image: "skif_per_ogjust_renuar", name: "Pierre-Auguste Renoir"), PainterView(image: "street_lafayette", name: "Edward Munch") ]
We’ll save biographies of artists locally for lack of a database.
Inside the body View, we’ll add a list:
var body: some View { VStack(spacing: 0) { HeroImage(name: "louvre_museum") NavigationView { List(painterData) { painter in NavigationLink(destination: PainterDetailView(painter: painter)) { PainterRow(painter: painter) } } .navigationBarTitle("") .navigationBarHidden(true) } } }
The list is created by iterating through the painterData array.
Our list is embedded in NavigationView so we can go and view the details of each row.
HeroImage is a structure for the custom image above a list:
struct HeroImage: View { let name: String var body: some View { Image(name) .resizable() .edgesIgnoringSafeArea(.top) .frame(height: 300) } }
PainterRow structure:
import SwiftUI struct PainterRow: View { @State var zoomed = false var painter: PainterView var body: some View { HStack { Image(painter.image).resizable() .frame(width: 50, height: 50, alignment: .leading) .overlay( Circle() .fill(self.zoomed ? Color.clear : Color(red: 1.0, green: 1.0, blue: 1.0, opacity: 0.4)) .saturation(self.zoomed ? 1 : 0) .scaleEffect(0.8) ) .shadow(radius: 10) .animation(.spring()) .onTapGesture { self.zoomed.toggle() } Text("\(painter.name)") .frame(width: 500, height: 20, alignment: .leading) } .font(.headline) } }
zoomed is a property to hold some of your state. Namely, it remembers whether the thumbnail is zoomed.
onTapGesture{...} is a method to toggle between zoomed in and zoomed out states.
Each time you tap on the thumbnail view, the zoomed state will alternate between true and false.
.scaleEffect is a modifier; each time you tap on the view it will alternate between 33% of its original size and 133% of its original size.
overlay adds another view as an overlay on your original view.
saturation modifies the color saturation of the current view. It will desaturate the view completely when it’s small and will animate it to full color when the view zooms in.
After clicking on a row in the list, we view the artist’s details.
import SwiftUI struct PainterDetailView: View { @State var zoomed = false var painter: PainterView var body: some View { ZStack { Color(red: 0.1, green: 0.1, blue: 0.1) .edgesIgnoringSafeArea(.all) HStack() { PainterTitle(title: painter.name, caption: biography) .offset(x: 0, y: -15) .padding(.leading, 30) .padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 0)) .offset(x: self.zoomed ? 500 : 0, y: -15) .animation(.default) .frame(width: 310, height: 400) Spacer() } VStack { GeometryReader() { geometry in Image(self.painter.image).resizable() .frame(width: 200, height: 200) .clipShape(RoundedRectangle(cornerRadius: self.zoomed ? 40 : 400 )) .overlay( Circle() .fill(self.zoomed ? Color.clear : Color(red: 1.0, green: 1.0, blue: 1.0, opacity: 0.4)) .saturation(self.zoomed ? 1 : 0) .scaleEffect(0.8) ) .position(x: self.zoomed ? geometry.frame(in: .local).midX : 640, y: 150) .scaleEffect(self.zoomed ? 1.73 : 0.33) .shadow(radius: 10) .animation(.spring()) .onTapGesture { self.zoomed.toggle() } } } } } }
The last step to get your animation going is adding animation(_) on the view you’d like to animate.
SwiftUI interpolates the changes between both animations. The final result is a curving motion that starts rightwards but turns around and ends to the left of its starting point. That builds a great zoom effect.
.animation(.spring()) is the default spring animation, which uses an oscillator under the hood with some default presets to deliver a well-honed timing curve for your animations.
overlay(_) takes another view as its parameter. You can create a circle inline and set its modifiers, then pass it to overlay(_). The circle overlay is 80% of the size of its parent and is filled with a semi-transparent white color. This makes it look more like a nice badge than a flat thumbnail. When you zoom in on the thumbnail, the circular overlay will disappear as you animate its white fill to a transparent color.
If you click repeatedly in quick succession, you’ll notice that interrupting the current animation never gives you a “broken” layout. That’s because all animations in SwiftUI are interruptible and reversible by default.
SwiftUI makes drawing and animating shapes really easy because you can apply the same basic principles to both views and shapes. You don’t need to differentiate between them, and they’re both first-class citizens of your UI.