In this article, we are going to explore the different options for using transitions. We’ll discuss several aspects of them. From the basic to the most advanced. How to configure, combine and trigger them. We’ll study what are the pre-existing transitions, but more importantly, how we can create our own. When doing so, the information from “Advanced SwiftUI Animations” will expand immensely the number of effects you can create. But first, the basic concepts, to make sure we are all on the same page:
What Is a Transition?
A transition in SwiftUI is what determines how a view is inserted or deleted from the hierarchy. A transition on its own has no effect. It must be associated with an animation. For example:
Note that since XCode 11.2, transitions no longer work with implicit animations. This means the following code only works with older versions of Xcode. Fortunately, the rest of the examples work fine. Thanks to Tyler Casselman for noticing and writing the comment!
struct ContentView: View {
@State private var show = false
var body: some View {
VStack {
Spacer()
if show {
LabelView()
.animation(.easeInOut(duration: 1.0))
.transition(.opacity)
}
Spacer()
Button("Animate") {
self.show.toggle()
}.padding(20)
}
}
}
struct LabelView: View {
var body: some View {
Text("Hi there!")
.padding(10)
.font(.title)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.green).shadow(color: .gray, radius: 3))
}
}
As mentioned above, since Xcode 11.2, transitions no longer work with implicit animations. If you need to understand the difference between implicit vs explicit animations, check the first part of “Advanced SwiftUI Animations“. For this case, you can use any of the two options below:
if show {
LabelView()
.transition(.opacity)
}
Spacer()
Button("Animate") {
withAnimation(.easeInOut(duration: 1.0)) {
self.show.toggle()
}
}.padding(20)
Another option is to associate an animation with a transition. Note that the animation is applied to the transition, not to the view (i.e., it’s inside .transition()).
if show {
LabelView()
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
}
Spacer()
Button("Animate") {
self.show.toggle()
}.padding(20)
Asymmetrical Transitions
By default, transitions apply in one way when the view is added to the hierarchy. When the view is removed, it will produce the opposite effect. For example, .opacity will fade in when adding the view, and fade out when removing it. However, this can be changed.
If we want to specify different transitions for adding/removing a view, we use the .asymmetric
option:
.transition(.asymmetric(insertion: .opacity, removal: .scale))
Combined Transitions
If you need to apply more than one effect during the transition, you can combine them. For example, to slide a view with a fade effect, you may add this transition:
.transition(AnyTransition.opacity.combined(with: .slide))
Note that you can also use .asymmetric
and .combined
together:
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .scale))
Transitions with Parameters
So far we’ve used transitions that receive no parameters: .opacity
, .slide
, .scale
. However, some transitions can be tuned with additional parameters. For example:
.scale(scale: 0.0, anchor: UnitPoint(x: 1, y: 0))
.scale(scale: 2.0)
.move(edge: .leading)
.offset(x: 30)
.offset(y: 50)
.offset(x: 100, y: 10)
Custom Transitions, the Fun Begins
Now that the basic stuff is out of the way, the fun part begins. This is where we can start to get creative. Buckle up!
How do Transitions work
Internally, both standard and custom transitions work in the same way. They need a modifier for the beginning and the end of the animation. SwiftUI will figure out the rest, provided the difference between both modifiers is animatable.
Let’s pretend the .opacity
transition didn’t exist and we need to create it. Let’s call our new custom transition: .myOpacity
. Here’s the implementation:
extension AnyTransition {
static var myOpacity: AnyTransition { get {
AnyTransition.modifier(
active: MyOpacityModifier(opacity: 0),
identity: MyOpacityModifier(opacity: 1))
}
}
}
struct MyOpacityModifier: ViewModifier {
let opacity: Double
func body(content: Content) -> some View {
content.opacity(opacity)
}
}
Then you just use the transition as usual:
.transition(.myOpacity)
As you can see, we simply create an extension to AnyTransition. In there, we create a new transition by specifying two modifiers, one for the beginning and another for the end. When removing the view, SwiftUI will use these modifiers inversely.
Transitions at Full Throttle
With what we know by now, we can create several new transitions. The existing animatable modifiers, such as .rotationEffect()
or .transformEffect()
open new possibilities.
As soon as you start creating new transitions though, you may find yourself a little restricted. That’s when all the knowledge from “Advanced SwiftUI Animations” can set you free. Especially useful are GeometryEffect and Shapes.
Custom Transitions with GeometryEffect
Transitions are especially well suited for presenting and dismissing panels. If you want to build your own modal system, you should seriously consider reading this next part.
For our next exercise, we will create a simple transition to demonstrate how to present and dismiss a view.
extension AnyTransition {
static var fly: AnyTransition { get {
AnyTransition.modifier(active: FlyTransition(pct: 0), identity: FlyTransition(pct: 1))
}
}
}
struct FlyTransition: GeometryEffect {
var pct: Double
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let rotationPercent = pct
let a = CGFloat(Angle(degrees: 90 * (1-rotationPercent)).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, 1, 0, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(pct * 2), y: CGFloat(pct * 2)))
if pct <= 0.5 {
return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
} else {
return ProjectionTransform(transform3d).concatenating(affineTransform1)
}
}
}
Code at: transiftion-present-dismiss.swift
Custom Transitions with Shapes
Another useful scenario for transitions is when we want to cross-transition between two views. One fades-in and the other fades-out. Our first instinct would be to put both views inside a ZStack and transition by changing their opacity. That is all right, but now we have the tools to do much more:
Code at: transiftion-present-dismiss.swift
This sample code requires you to add 4 images to your asset catalog (name them photo1, photo2, photo3 and photo4). This sample is designed for an iPad in landscape orientation.
You can check the code in the gist file, but all transitions follow a common pattern. They all use an animated shape to clip the incoming and outgoing images. Since both are z-stacked, we get a nice cross-effect.
In Summary
In this article, we uncover the tools you need to create your own SwiftUI transitions. Now you only need to set your imagination free and start creating your own cool effects.
Feel free to comment, and follow me on twitter if you want to be notified when new posts are published. Until next time.
Hi, Javier! Great article!
But the first example with implicit animation is not working in Xcode 11.2 beta. Maybe it is Xcode bug.
Hi, is there a simple way to remove transitions entirely? For example, I want to remove the show/hide on fade transition on text in buttons when clicked?
The “no transition” transition is
.transition(AnyTransition.identity)
Hi Javier,
Great article as always, thanks
I didn’t quite understand how to implement slide with scale, please add sample code gist for
.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .scale))
Hi Javier,
Amazing article.
Learned so many magical things here.
One question I have is that if you have a VStack and there are a bunch of child views in the VStack. Is it possible to give different transitions to the child views, we can only give a transition animation to the parent view only.
For example, I have a Rectangle and a circle in a VStack. When the view appears, I want the circle to fade, but the rectangle should slide.
I tried it, but it doesn’t seem to work. Any ideas.
Also if it helps, my child views are more complex than a simple circle, they have their own children.
Hi Sarang, thank you for your comment. There probably is a way of doing what you are asking, but I haven’t investigated it yet. As a workaround, you can probably use the onAppear and onDisappear handlers.
works on 11.3 if its called with AnyTransition.customTransitionName:
.transition(AnyTransition.fly.animation(.easeInOut(duration: 1.0)))
probably a swift 5.1 thing needing more specificity
Hi Javier…..
Its an Excellent article, I have a small suggestion
in picture show : transiftion-present-dismiss.swift
we can write like this ,
var transition: AnyTransition {
switch transitionType {
case 0 :
return .opacity
case 1:
return .scale
case 2:
return .circular
case 3:
return .rectangular
case 4:
return .stripes(stripes: 50, horizontal: true)
case 5:
return .stripes(stripes: 50, horizontal: false)
default:
return .opacity
}
instead of else if , no need check for each statement
Yes! You are right, but there was a problem in a past Xcode beta that did not like the switch mixed with transitions (weird!). The problem is probably gone, and I should probably update the code though…
Looks like the first example (with implicit animation) no longer works on latest swiftUI version. The view appears immediately. Using withAnimation or adding the animations to the transitions still works though.
You’re right. I updated the article. Thanks!
Hello Javier, I have a question about the SwiftUI transition.
I have the following view,
What I don’t understand is that when the VStack view appears, it is animated with the defined transition, but when it disappears (set show to false), it is not animated.
Do you have any ideas? Thanks in advance for any help.
Since at least Xcode 11.2, transitions do not work well with implicit animations. Try something like this:
withAnimation { self.show.toggle() {
Hello Javier,
Thank you for letting me know this change in 11.2. However, the explicit animation does not work either with my setup. Fortunately, I did some additional testing, I think I found the root of this issue.
What I am trying to do is actually a toast-style alert and a half-screen sheet. This is why I have …
Any my observation is if my main content is not NavigationView, the transition is always correctly animated. But if it is on top of a NavigationView, the toast or half sheet will disappear without being animated.
The full testing code is here.
https://gist.github.com/cedric-elca/d1ebf92ef8ff8c62039e8537125a7b48
Hi Javier (and all the other guys),
I’m still having trouble with transition animations (this feature seems really buggy). It seems that even attaching an animation directly to a transition won’t work in some cases (for instance with the .slide transition). I don’t know if any of you has already experienced this kind of bug. I asked this in StackOverflow today: https://stackoverflow.com/questions/59116958/swiftui-attach-an-animation-to-a-transition
Thanks for any help.
Matteo
Yes. It’s true. I also noticed that some types of transitions do not work well with animations attached directly to the transition. Applied explicitly, however, they work fine.
Hi Javier!
I love your tutorials! Thanks so much for sharing your knowledge 😀
I’m sorry, I’m new to SwiftUI and I’m a bit confused… what’s the difference (if any) between using .transition(.opacity) on a view and animating a view’s opacity with a @State var? Lets say that for either of both, you click a button to hide/show a view.
Thanks!
Although the effect may be similar, they’re different:
1. A transition is an animation that occurs when you add or remove a view from the view hierarchy. Using an opacity transition fades the view in and out. But when the opacity reaches 0, the view is no longer there.
2. Changing the opacity of a view, does not insert or remove the view from the view hierarchy. When the opacity is 0, the view is still there, just invisible. It will also continue to affect the layout of the surrounding views, because it still occupies its space.
Cheers,
Javier.-
Hi Javier!
Thank you for the great tutorial. I’ve found it when I stuck with Apple Guide for animating views and transitions: https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions
Section 4 (https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions#Compose-Animations-for-Complex-Effects) contains a simple logic that doesn’t work — it applies animation and transition for the first appearance but then it blocks the subview from being updated.
In the code below .transition(.slide) adds transition effect for GraphCapsule. And it nicely appears for the first time with .animation(.ripple()) effect. But then, when you switch the data source for GraphCapsule — it only animated, but not updated with new data.
I’ve spent a few days trying to find out what’s wrong with that and can’t give up with no answer. Could you have a look at that?
Here is a code:
“`
var body: some View {
let data = hike.observations
let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] })
let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
let heightRatio = (1 – CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2
return GeometryReader { proxy in
HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
ForEach(data.indices) { index in
GraphCapsule(
index: index,
height: proxy.size.height,
range: data[index][keyPath: self.path],
overallRange: overallRange)
.colorMultiply(self.color)
.transition(.slide)
.animation(.ripple())
}
.offset(x: 0, y: proxy.size.height * heightRatio)
}
}
}
“`
Here is also a dev forum thread for that with no answer: https://developer.apple.com/forums/thread/131041
Hi Eugene, sorry for the delay in my reply, but the latest WWDC2020 keeps me busy. I posted a reply in the forum thread you mentioned. Bottom line, the tutorial has a bug. Check my reply for more details. Cheers.
Thank you, Javier. No rush with that, I’ve just put that away for a while.
I’ve tried your solution, and it didn’t work. But thanks to your reference to release notes – I’ve found the way to make it working and posted the solution in the forum thread.
Do you have an idea why it doesn’t work only when .transition is applied? Seems to be related to some view wrapping by transition modifier, but I’m not sure how exactly.
Whoo, thanks for this. After going through various sites including stackoverflow, this article solved my issues. Thank again
WOW, very nice, very profound. Stunning!
Thanks very much for sharing !
Great Article! thanks!
Javier,
I believe implicit animation *do* work on Xcode later than 11.2, but with a *strange glitch*:
.transition(.scale) // NOT WORKING
but
.transition(.scale.animation(.default)) // WORKS!
* verified on Xcode 12 & 13(beta2)