I’m trying to figure out how to change the height of a SwiftUI view with animation, without affecting the position of its subviews. I have an HStack containing some text objects. I want to animate the height of the view from 0 to 40 like so:
Note that the text objects shouldn't move as the parent view height changes (they should be anchored to the top of the parent view). I’ve tried the standard transitions such as: .transition(.scale(scale: 0, anchor: UnitPoint.top)). But that scales the entire view, including the subviews.
Here’s the view code:
struct SampleView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
let strings = ["Text 1", "Text 2", "Text 3"]
ForEach(strings, id: \.self) { string in
Text(string)
}
}
.foregroundStyle(Color.white)
}
.background(Color.orange)
}
}
An instance of this SampleView is created when a button action withAnimation block updates a variable.
Any suggestions?
I’m trying to figure out how to change the height of a SwiftUI view with animation, without affecting the position of its subviews. I have an HStack containing some text objects. I want to animate the height of the view from 0 to 40 like so:
Note that the text objects shouldn't move as the parent view height changes (they should be anchored to the top of the parent view). I’ve tried the standard transitions such as: .transition(.scale(scale: 0, anchor: UnitPoint.top)). But that scales the entire view, including the subviews.
Here’s the view code:
struct SampleView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
let strings = ["Text 1", "Text 2", "Text 3"]
ForEach(strings, id: \.self) { string in
Text(string)
}
}
.foregroundStyle(Color.white)
}
.background(Color.orange)
}
}
An instance of this SampleView is created when a button action withAnimation block updates a variable.
Any suggestions?
Share Improve this question edited Feb 4 at 8:27 Benzy Neez 22.3k3 gold badges15 silver badges42 bronze badges asked Feb 4 at 7:02 RobertoRoberto 4136 silver badges9 bronze badges2 Answers
Reset to default 2You can create a custom transition that scales a mask
of the view.
struct RevealTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content.mask {
Rectangle()
.scaleEffect(y: phase.isIdentity ? 1 : 0, anchor: .top)
}
}
}
extension AnyTransition {
static var reveal: AnyTransition { AnyTransition(RevealTransition()) }
}
Example usage:
@State private var show = false
var body: some View {
VStack {
if show {
SampleView()
.transition(.reveal)
}
Button("Animate") {
show.toggle()
}
}
.animation(.default, value: show)
}
If you want the view to disappear in a different way, edit the body
as needed, or use an asymmetric
transition.
If the content that is being revealed is inside a VStack
and if it is followed by content with an opaque background (in other words, if it is followed by content that will cover it during the transition), then the reveal works this way as standard. Normally it is accompanied by an .opacity
transition, but you can override this by applying a .transition
modifier.
If you want to enforce a fixed height and if you want the revealed content to have an orange background for this full height, you either need to apply the fixed height inside SampleView
before applying the orange background, or you can apply an orange background to the surrounding container too (the VStack
). However, a better approach might be to let the revealed view determine its own height.
struct ContentView: View {
@State private var isShowing = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("The quick brown fox")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.background)
if isShowing {
SampleView()
.frame(height: 40)
.transition(.asymmetric(
insertion: .identity,
removal: .opacity
))
}
Text("jumps over the lazy dog")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.background)
}
.background(.orange)
.frame(height: 100, alignment: .top)
.onTapGesture {
withAnimation(.easeInOut(duration: 3)) {
isShowing.toggle()
}
}
}
}