最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - Animate a height and width change separately in a SwiftUI view - Stack Overflow

programmeradmin5浏览0评论

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
Add a comment  | 

2 Answers 2

Reset to default 1

maybe 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 ViewModifiers 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.

发布评论

评论列表(0)

  1. 暂无评论