WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Day 33 - Building custom transitions using ViewModifier - question about animation behavior

Forums > 100 Days of SwiftUI

https://www.hackingwithswift.com/books/ios-swiftui/building-custom-transitions-using-viewmodifier

in this tutorial, when Paul comments the .clipped() method, we can se the red rectangle sliding in and out from the screen.

But why when is sliding out it goes behind the blue rectangle first? and how could I make the red rectangle slide form the front to be able to see it sliding out from the screen?

   

@igor has his logic back-to-front and seeks clarity:

But why when is sliding out it goes behind the blue rectangle first?

Eat the elephant

First break your big problem into smaller problems and solve them one at a time. Eat the big elephant one bite at a time.

When you remove the clipped() modifier, you will see both squares on the screen at the same time! One is coming from outerspace and is coming INTO the view. The other is already ON the view but will soon disappear into the vast, timeless land of nothingness.

To get a better grasp on this, run your code in the simulator, and select the Slow Animations menu item in the Debug menu. You'll notice how one view will start from the TOP of the screen then rotate clockwise into position.

The other square will start in the normal position, but will rotate anti-clockwise from the bottom to the top of the screen. Eventually, it will disappear from the view.

Why clockwise? (the next bite)

Why does the view at the top of the screen turn clockwise?
Take a close look at the transition. The transition's goal is to get the square turned to the ZERO degree postion, relative to the top leading corner. It starts at the -90 degree position. So it's starting position is laying on its leading side, but it has to end up in the center of the screen, but standing straight up.

To do this it must start above its ending postion, and must rotate clockwise into position.

Why anti-clockwise? (the next bite)

Why does the view at the bottom of the screen turn anti-clockwise? Take a look again at the transition. While in motion, its goal is to finish -90 degrees from its starting position. Since it's already on the screen, to finish at -90 degrees, it must rotate anti-clockwise.

Notice that after it gets to its final position, the square's animation is over, so it's no longer on the screen. It disappears.

Why does the top square always rotate on TOP of the bottom square?

This is a function of the ZStack. Remember, the first object in a ZStack is drawn in the front of the other ZStack items. During animation, one view is On Screen, whilst the other is rotating OFF Screen. Consequently, you can think of the ONE item ON screen will always be the TOP item, everything else is behind it.

Visualize!

Here's some similar code. However I added a text box overlay to each square. You can see how the text is rotated to visualize where the square's TOP LEADING corner is.

struct CornerRotateViewModifier: ViewModifier {
    let amount: Double
    let anchor: UnitPoint

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

extension AnyTransition {
    static var pivot: AnyTransition {
        // starting position is rotated -90 degrees relative to top leading corner
        // finished position is rotation 0, relative to the top leading corner
        .modifier(active:   CornerRotateViewModifier(amount: -90, anchor: .topLeading),
                  identity: CornerRotateViewModifier(amount: 0, anchor: .topLeading))
    }
}

struct ClippedViewModifierView: View {
    @State private var isShowingRed = true

    var body: some View {
        VStack {
            ZStack {
                // Which of these two views is TOP view in the ZStack?
                if isShowingRed {
                    Rectangle()
                        .fill(.red)
                        .frame(width: 200, height: 200)
                        .transition(.pivot)
                        .overlay { Text("Red") }
                } else {
                    Rectangle()
                        .fill(.mint)
                        .overlay { Text("Mint") }
                        .frame(width: 200, height: 200)
                        .transition(.pivot)
                }
            }
            .onTapGesture {
                withAnimation { isShowingRed.toggle() }
            }
            Text("Showing Red is: \(isShowingRed)").animation(.none)
        }
    }
}

Keep Coding!

What's this about eating an elephant? Here's some links to my advice on eating elephants.

See -> How to Eat an Elephant

   

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.