Advanced SwiftUI Transitions

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:

Transitions Sample 1

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:

Sample 2
.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 sample 3
.transition(AnyTransition.opacity.combined(with: .slide))

Note that you can also use .asymmetric and .combined together:

Sample 4
.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.

24 thoughts on “Advanced SwiftUI Transitions”

  1. Hi, Javier! Great article!
    But the first example with implicit animation is not working in Xcode 11.2 beta. Maybe it is Xcode bug.

    Reply
  2. 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))

    Reply
  3. 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.

    Reply
    • 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.

      Reply
      • 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

        Reply
  4. 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

    Reply
    • 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…

      Reply
  5. 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.

    Reply
  6. Hello Javier, I have a question about the SwiftUI transition.

    I have the following view,

    ZStack(.bottom) {
        ...
        if show {    // show is a state
               VStack {
                     Text{}
                     ...
               }
               .animation(...)
               .transition(.move(.bottom))
        }
    }
    

    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.

    Reply
    • Since at least Xcode 11.2, transitions do not work well with implicit animations. Try something like this:

      withAnimation { self.show.toggle() {

      Reply
      • 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 …

        ZStack(.bottom) {
        
            // Main Content
            if show {
                Text{}
            }
        

        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

        Reply
  7. 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

    Reply
    • 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.

      Reply
  8. 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!

    Reply
    • 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.-

      Reply
  9. 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

    Reply
    • 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.

      Reply
      • 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.

        Reply
  10. Whoo, thanks for this. After going through various sites including stackoverflow, this article solved my issues. Thank again

    Reply
  11. 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)

    Reply

Leave a Comment

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close