animationswiftuitransitionxcode15ios17

How to move ZStack from bottom when View is presented SwiftUI iOS 17.4?


I'm trying to show ZStack with animation from bottom when View is presented on screen. Code below doesn't work. I'm new and I don't know what is wrong with this? Where the withAnimation should be, and where transition ?

Code looks like this:

                ZStack {
                    RoundedRectangle(cornerRadius: 15, style: .continuous)
                        .fill(.white)
                        .frame(width:UIScreen.main.bounds.width - 32, height: 250)
                        .background(.ultraThinMaterial,
                                    in: RoundedRectangle(cornerRadius: 15, style: .circular))
                        .opacity(0.4)
                        .shadow(radius: 5, x:0, y: 3)
                    
                    VStack(alignment: .leading, spacing: 10) {
                        Text("Login")
                            .font(.title3)
                            .foregroundColor(Color(.systemGray))
                        
                        TextField("Enter login", text: $login)
                            .textFieldStyle(.roundedBorder)
                            .submitLabel(.continue)
                        
                        Text("Password")
                            .font(.title3)
                            .foregroundColor(Color(.systemGray))
                        
                        SecureField("Enter password", text: $password)
                            .textFieldStyle(.roundedBorder)
                            .submitLabel(.done)
                        Spacer()
                        
                        Button(action: {
                            
                        }, label: {
                            Text("Enter")
                                .frame(maxWidth: .infinity, maxHeight: 40)
                                .background(Color(.systemYellow))
                                .clipShape(.buttonBorder)
                                .foregroundStyle(Color(.black))
                                .opacity(0.6)
                        })
                    }
                    .padding()
                    .frame(width:UIScreen.main.bounds.width - 32, height: 250)
                }
                .padding()
                .onAppear {
                    withAnimation(.easeInOut(duration: 5)) {
                        isPresented.toggle()
                    }
                }
                .transition(AnyTransition.move(edge: .bottom))
                .zIndex(0)
            }




Solution

  • There is no transition happening because the content is already visible when it first appears.

    To fix:

    Currently, the flag isPresented is being toggled in .onAppear. There is a possibility, that this may cause it to disappear when presented a second time (depending on whether the state variable retains its state). I would suggest setting it explicitly to true instead. You could use an .onDisappear callback to set it back to false, if you need to.

    The zIndex might not be needed either, try deleting it.

    Group {
        if isPresented {
            ZStack {
                // ...
            }
            .padding()
            .transition(AnyTransition.move(edge: .bottom))
        }
    }
    .onAppear {
        withAnimation(.easeInOut(duration: 5)) {
            isPresented = true
        }
    }
    .onDisappear {
        isPresented = false
    }
    

    EDIT Following from your comment, here is a fully updated standalone example to demonstrate it working.

    On the issue of the flag, I suspect that what is happening is that you are toggling isPresented somewhere else in your code. In the example below, I have deliberately renamed the flag to isShowing, so that there is no chance of it being updated from elsewhere.

    Regarding the animation, if you want the panel to slide up from the bottom edge of the screen then extra (empty) height needs to be added, equivalent to the space between the panel and the bottom edge. This extra height can be computed if the height of the screen is known.

    The updated code below also includes the following changes:

    struct ContentView: View {
        private let panelHeight: CGFloat = 250
        @State private var isShowing = false
        @State private var login = ""
        @State private var password = ""
        var body: some View {
            GeometryReader { screen in
                if isShowing {
                    VStack(alignment: .leading, spacing: 10) {
                        Text("Login")
                            .font(.title3)
                            .foregroundStyle(.gray)
    
                        TextField("Enter login", text: $login)
                            .textFieldStyle(.roundedBorder)
                            .submitLabel(.continue)
    
                        Text("Password")
                            .font(.title3)
                            .foregroundStyle(.gray)
    
                        SecureField("Enter password", text: $password)
                            .textFieldStyle(.roundedBorder)
                            .submitLabel(.done)
    
                        Spacer()
    
                        Button {} label: {
                            Text("Enter")
                                .frame(maxWidth: .infinity, maxHeight: 40)
                                .background(.yellow)
                                .clipShape(.buttonBorder)
                                .foregroundStyle(.black)
                                .opacity(0.6)
                        }
                    }
                    .padding()
                    .frame(height: panelHeight)
                    .frame(maxWidth: .infinity)
                    .background {
                        RoundedRectangle(cornerRadius: 15)
                            .fill(Color(white: 0.94))
                            .shadow(radius: 5, x:0, y: 3)
                    }
                    .padding(.horizontal, 16)
                    .frame(
                        height: (screen.size.height + panelHeight) / 2,
                        alignment: .bottom
                    )
                    .transition(.move(edge: .bottom))
                }
            }
            .ignoresSafeArea()
            .onAppear {
                withAnimation(.easeInOut(duration: 5)) {
                    isShowing = true
                }
            }
            .onDisappear {
                isShowing = false
            }
        }
    }
    

    Animation