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)
}
There is no transition happening because the content is already visible when it first appears.
To fix:
ZStack
inside some other container (a Group
will do).onAppear
callback to the container.ZStack
conditional on the state variable isPresented
.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:
GeometryReader
to measure it, because UIScreen.main
is deprecated. Also, UIScreen.main
doesn't work well with iPad split-screen. So the top-level container is now a GeometryReader
.GeometryReader
is only needed for finding the height of the screen. If you want the panel to use the full width - 32pt, then you can just set maxWidth: .infinity
and apply horizontal padding of 16pt.ZStack
, the RoundedRectangle
can simply be applied as .background
to the VStack
. This way, it automatically has the same size..foregroundColor
is also deprecated, use .foregroundStyle
instead.Color
from a UIColor
. For the standard colors, you can just use the definitions provided by Color
.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
}
}
}