Search code examples
swiftuitransitionswiftui-animation

Animated transitions and contents within ScrollView in SwiftUI


I'm not quite a SwiftUI veteran but I've shipped a couple of apps of moderate complexity. Still, I can't claim that I fully understand it and I'm hoping someone with deeper knowledge could shed some light on this issue:

I have some content that I want to toggle on and off, not unlike .sheet(), but I want more control over it. Here is some "reconstructed" code but it should be able capture the essence:

struct ContentView: View {

    @State private var isShown = false

    var body: some View {

        GeometryReader { g in

            VStack {

                ZStack(alignment: .top) {

                    // This element "holds" the size
                    // while the content is hidden
                    Color.clear

                    // Content to be toggled
                    if self.isShown {
                        ScrollView {
                            Rectangle()
                                .aspectRatio(1, contentMode: .fit)
                                .frame(width: g.size.width) // This is a "work-around"
                        } // ScrollView
                            .transition(.move(edge: .bottom))
                            .animation(.easeOut)
                    }

                } // ZStack

                // Button to show / hide the content
                Button(action: {
                    self.isShown.toggle()
                }) {
                    Text(self.isShown ? "Hide" : "Show")
                }

            } // VStack

        } // GeometryReader

    }
}

What it does is, it toggles on and off some content block (represented here by a Rectangle within a ScrollView). When that happens, the content view in transitioned by moving in from the bottom with some animation. The opposite happens when the button is tapped again.

This particular piece of code works as intended but only because of this line:

.frame(width: g.size.width) // This is a "work-around"

Which, in turn, requires an extra GeometryReader, otherwise, the width of the content is animated, producing an unwanted effect (another "fix" I've discovered is using the .fixedSize() modifier but, to produce reasonable effects, it requires content that assumes its own width like Text)

My question to the wise is: is it possible to nicely transition in content encapsulated within a ScrollView without using such "fixes"? Alternatively, is there a more elegant fix for that?

A quick addition to the question following @Asperi's answer: contents should remain animatable. You are my only hope,

–Baglan


Solution

  • Here is a solution (updated body w/o GeometryReader). Tested with Xcode 11.4 / iOS 13.4

    demo

    var body: some View {
        VStack {
            ZStack(alignment: .top) {
                // This element "holds" the size
                // while the content is hidden
                Color.clear
    
                // Content to be toggled
                if self.isShown {
                    ScrollView {
                        Rectangle()
                            .aspectRatio(1, contentMode: .fit)
                            .animation(nil)     // << here !!
                    } // ScrollView
                        .transition(.move(edge: .bottom))
                        .animation(.easeOut)
                }
            } // ZStack
    
            // Button to show / hide the content
            Button(action: {
                self.isShown.toggle()
            }) {
                Text(self.isShown ? "Hide" : "Show")
            }
        } // VStack
    }