TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

How does .asymmetric's insertion and removal works?

Forums > SwiftUI

Hi devs so i was studying custom transitions. After some hurdles I am able to make custom transition work. But now I have a question.

how does insertion and removal work really in .asymmetric function?

extension AnyTransition {
    static func pivot(withDelay delay: Double = 0.0) -> AnyTransition {
        let insertion =
        AnyTransition.modifier(
            active: CornerRotateModifier(amount: -90, anchor: .topLeading),
            identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
        )
        .animation( Animation.easeInOut(duration: 0.5)
            .delay(delay)
        )
        let removal =
        AnyTransition.modifier(
            active: CornerRotateModifier(amount: -90, anchor: .topLeading),
            identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
        )
        .animation( Animation.easeInOut(duration: 0.5)
            .delay(delay)
        )

        return .asymmetric(insertion: insertion, removal: removal)

    }

}

according to this code initially what I understood was that (in case of insertion) active is the state of UI from where we want to start and identity is the state of UI to where we want to reach. which seems understandable. but then in case of removal this doesn't seem to match. I would assume that values of insertion and removal would be totally opposite to make exit animation work. but it breaks things. but if , we add same values as insertion or identity is preserved, then it works fine.

This relation is something I am not able to wrap my head around.

so is this like this? Image Link to Image

   

struct CornerRotateModifier: ViewModifier {
    let amount: Double;
    let anchor: UnitPoint;

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(amount), anchor: anchor)
            .clipped()
    }
}

extension AnyTransition {
    static func pivot() -> AnyTransition {
        .asymmetric(
            insertion: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            ),
            removal: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            )
        )
    }
}

struct ContentView: View {
    @State private var isShowingRed = false
    @State private var delay: Double = 1

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 200, height: 200)

            if isShowingRed {
                Rectangle()
                    .fill(.red)
                    .frame(width: 200, height: 200)
                    .transition(.pivot())
                    .zIndex(1) // You need zIndex to help keep the view on top during removal
            }

        }
        .animation(Animation.easeInOut(duration: 0.5).delay(delay), value: isShowingRed)
        .onTapGesture {
            isShowingRed.toggle()
        }
    }
}

or ContentView like so:

struct ContentView: View {
    @State private var isShowingRed = false

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 200, height: 200)

            if isShowingRed {
                Rectangle()
                    .fill(.red)
                    .frame(width: 200, height: 200)
                    .transition(.pivot())
                    .zIndex(1) // You need zIndex to help keep the view on top during removal
            }

        }
        .onTapGesture {
            withAnimation(Animation.easeInOut(duration: 0.5).delay(1)) {
                isShowingRed.toggle()
            }
        }
    }
}

below will work without zIndex, make sure to run on simulator

extension AnyTransition {
    static func pivot(withDelay delay: Double = 0.0) -> AnyTransition {
        .asymmetric(
            insertion: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            )
            .animation(Animation.easeInOut(duration: 0.5).delay(delay))
            ,
            removal: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            )
            .animation(Animation.easeInOut(duration: 0.5).delay(delay))
        )
    }
}

struct ContentView: View {
    @State private var isShowingRed = false

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 200, height: 200)
                .overlay {
                    if isShowingRed {
                        Rectangle()
                            .fill(.red)
                            .frame(width: 200, height: 200)
                            .transition(.pivot(withDelay: 1))
                    }
                }
        }
        .onTapGesture {
            isShowingRed.toggle()
        }
    }
}

   

Hi @ygeras

Thanks for such detailed answer, but it looks like this answer is for another question I posted yesterday. Which still answer's that and I got to learn more about it.

but can you please help me understand this code.

.asymmetric(
            insertion: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            )
            .animation(Animation.easeInOut(duration: 0.5).delay(delay))
            ,
            removal: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
            )
            .animation(Animation.easeInOut(duration: 0.5).delay(delay))
        )

how does insertion and removal behave really? I would think identity of insertion and removal woud be different.

   

I would advise to add border to frames or comment out clipped shapes to see the whole picture what is going on on the screen. Hope this sample will shed some light on how it moves around so that you have better understanding. Compare it with built-in transition as well.

RUN ON SIMULATOR.

struct CornerRotateModifier: ViewModifier {
    let amount: Double;
    let anchor: UnitPoint;

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(amount), anchor: anchor)
//            .clipped()
    }
}

extension AnyTransition {
    static func pivot(withDelay delay: Double = 0.0) -> AnyTransition {
        .asymmetric(
            insertion: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading), // Appears on screen
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading) // Moves to its identity location
            )
            .animation(Animation.easeInOut(duration: 3).delay(delay))
            ,
            removal: .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .bottomTrailing),// Appears on screen - already here
                identity: CornerRotateModifier(amount: 0, anchor: .bottomTrailing) // Moves to its identity location
            )
            .animation(Animation.easeInOut(duration: 3).delay(delay))
        )
    }
}

struct ContentView: View {
    @State private var isShowingRed = false
    @State private var isShowingPurple = false

    var body: some View {
        VStack {
            Text("Pivot Transition")
            ZStack {
                Rectangle()
                    .fill(.blue)
                    .frame(width: 200, height: 200)
                    .overlay {
                        if isShowingRed {
                            Rectangle()
                                .fill(.red)
                                .frame(width: 200, height: 200)
                                .transition(.pivot(withDelay: 1))
                        }
                    }
            }
            .onTapGesture {
                isShowingRed.toggle()
            }

            Text("Assymetric Transition")
            ZStack {
                Rectangle()
                    .fill(.blue)
                    .frame(width: 200, height: 200)
                    .overlay {
                        if isShowingPurple {
                            Rectangle()
                                .fill(.purple)
                                .frame(width: 200, height: 200)
                                .transition(.asymmetric(insertion: .scale, removal: .slide))
                            // if you place .scale to removal
                            // it will "reverse" upon disappear
                        }
                    }
            }
            .onTapGesture {
                withAnimation(.easeInOut(duration: 3).delay(0.5)) {
                    isShowingPurple.toggle()
                }
            }

        }
    }
}

   

@ygeras I ran the code and was playing around with it to understand how insertion and removal works.

extension AnyTransition {
    static func pivot(withDelay delay: Double = 0.0) -> AnyTransition {
        .asymmetric(
            insertion: .modifier(
                active: CornerRotateModifier(amount: 90, anchor: .topLeading), // Appears on screen
                identity: CornerRotateModifier(amount: 0, anchor: .topLeading) // Moves to its identity location
            )
            .animation(Animation.easeInOut(duration: 3).delay(delay))
            ,
            removal:
                    .modifier(
                active: CornerRotateModifier(amount: -90, anchor: .topLeading),// Appears on screen - already here
                identity: CornerRotateModifier(amount: 45, anchor: .topLeading) // Moves to its identity location
            )
            .animation(Animation.easeInOut(duration: 1).delay(delay))
        )
    }
}

I changed this code to have different identity for insertion and removal. what i am seeing is the both identity are kind of combined ?! it starts off at 135 degrees and stops at 45 degrees (I had to measure that using protractor 😅).

Am I wrong?

edit: and yes, on removal red box goes to -90

   

Seems like so, I cannot tell with all those rotation effects, did not dig so deep in documentation for that. Usually, I try to use different scenarios to get the effect I need. That's the beauty of high level code ))) we usually do not dig into e.g. sort option in swift, we can understand the general logic and efficiency of the code and in most cases that suffice.

   

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.