I'm trying to animate the height and widths of a view with different animation durations, but it just chooses one animation duration. How is this done, given an initial width and height, and final width and height?
An example of what I've tried and thought would work but doesn't.
struct TestView: View {
@State var currentWidth: CGFloat = 300
@State var currentHeight: CGFloat = 100
var body: some View {
VStack {
Color.purple
.frame(width:currentWidth, height: currentHeight)
.animation(.spring(duration:1.0), value:currentWidth)
.animation(.spring(duration:20.0), value:currentHeight)
}
.onAppear {
currentWidth = 450
currentHeight = 900
}
}
}
struct TestView: View {
@State var currentWidth: CGFloat = 300
@State var currentHeight: CGFloat = 100
var body: some View {
VStack {
Color.purple
.frame(width:currentWidth, height: currentHeight)
}
.onAppear {
withAnimation(.spring(duration:1.0)) {
currentWidth = 450
}
withAnimation(.spring(duration:20.0)) {
currentHeight = 900
}
}
}
}
I'm trying to animate the height and widths of a view with different animation durations, but it just chooses one animation duration. How is this done, given an initial width and height, and final width and height?
An example of what I've tried and thought would work but doesn't.
struct TestView: View {
@State var currentWidth: CGFloat = 300
@State var currentHeight: CGFloat = 100
var body: some View {
VStack {
Color.purple
.frame(width:currentWidth, height: currentHeight)
.animation(.spring(duration:1.0), value:currentWidth)
.animation(.spring(duration:20.0), value:currentHeight)
}
.onAppear {
currentWidth = 450
currentHeight = 900
}
}
}
struct TestView: View {
@State var currentWidth: CGFloat = 300
@State var currentHeight: CGFloat = 100
var body: some View {
VStack {
Color.purple
.frame(width:currentWidth, height: currentHeight)
}
.onAppear {
withAnimation(.spring(duration:1.0)) {
currentWidth = 450
}
withAnimation(.spring(duration:20.0)) {
currentHeight = 900
}
}
}
}
Share
Improve this question
asked Apr 1 at 3:06
AntcDevAntcDev
897 bronze badges
2 Answers
Reset to default 1maybe you need use Keyframe Animation
struct ContentView: View {
@State private var start = false
var body: some View {
VStack {
Button {
start.toggle()
} label: {
Text("Trigger")
}
Color.red
.keyframeAnimator(
initialValue: 0,
trigger: start,
content: { content, value in
content.frame(
width: min(200, 100 + value * 10),
height: 100 + value * 10
)
},
keyframes: { value in
LinearKeyframe(30, duration: 0.5, timingCurve: .easeOut)
}
)
}
}
}
You should use the animation
modifier that take a body
closure. This is how you separate animations into different "domains". Rather than animating a value change, it only animates the ViewModifier
s in the body
that are Animatable
.
Unfortunately, frame
is not an Animatable
modifier (but scaleEffect
is, so consider using that if you can), so you need to make an Animatable
wrapper around it.
struct AnimatableFrameModifier: ViewModifier, Animatable {
var animatableData: CGFloat
let isWidth: Bool
init(width: CGFloat) {
self.animatableData = width
isWidth = true
}
init(height: CGFloat) {
self.animatableData = height
isWidth = false
}
func body(content: Content) -> some View {
content.frame(width: isWidth ? animatableData : nil,
height: isWidth ? nil : animatableData)
}
}
struct TestView: View {
@State var currentWidth: CGFloat = 10
@State var currentHeight: CGFloat = 10
var body: some View {
VStack {
Color.purple
.animation(.spring(duration:1.0)) {
$0.modifier(AnimatableFrameModifier(width: currentWidth))
}
.animation(.spring(duration:20.0)) {
$0.modifier(AnimatableFrameModifier(height: currentHeight))
}
}
.onAppear {
currentWidth = 100
currentHeight = 100
}
}
}
The alternative that uses scaleEffect
, which might be undesirable depending on what you are doing.
struct TestView: View {
@State var currentWidth: CGFloat = 10
@State var currentHeight: CGFloat = 10
@State var xScale: CGFloat = 1
@State var yScale: CGFloat = 1
var body: some View {
VStack {
Color.purple
.frame(width: 10, height: 10)
.animation(.spring(duration:1.0)) {
$0.scaleEffect(x: xScale)
}
.animation(.spring(duration:20.0)) {
$0.scaleEffect(y: yScale)
}
.frame(width: currentWidth, height: currentHeight)
}
.onAppear {
currentWidth = 100
currentHeight = 100
xScale = 10
yScale = 10
}
}
}
Before iOS 17, you can achieve a similar effect by waiting for a short amount of time between two withAnimation
calls.
@State var currentWidth: CGFloat = 10
@State var currentHeight: CGFloat = 10
var body: some View {
VStack {
Color.purple
.frame(width: currentWidth, height: currentHeight)
}
.task {
withAnimation(.linear(duration: 1)) {
currentWidth = 100
}
// here I used Task.yield to wait for the next frame
// you can also use Task.sleep, DisptachQueue.main.asyncAfter, etc
await Task.yield()
withAnimation(.linear(duration: 2)) {
currentHeight = 400
}
}
}
But this only works for animations that merge in a desirable way. The way spring
animations merge are not desirable for this purpose.