如何在SwiftUI中停止当前状态下的动画



你好,我写了一个简单的代码,用一个改变其位置的圆圈创建了一个动画。当我点击那个圆时,我希望动画停止,但它不会停止,无论我将动画设置为零。也许我的方法在某种程度上是错误的。如果有人帮忙,我会很高兴的。

struct ContentView: View {
@State private var enabled = true
@State private var offset  = 0.0

var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onAppear {
withAnimation(self.enabled ? .easeIn(duration: 5) : nil) {
self.offset = Double(-UIScreen.main.bounds.size.height + 300)
}
}
.onTapGesture {
self.enabled = false
}
}
}

如@Schottky所述,offset值已经设置,SwiftUI只是为更改设置动画。

问题是,当您敲击圆圈时,不会再次调用onAppear,这意味着根本不会检查enabled,即使是这样,也不会阻止offset设置动画。

为了解决这个问题,我们可以引入Timer并进行一些小的计算,iOS附带了一个内置的Timer类,可以让我们定期运行代码。所以代码看起来是这样的:

struct ContentView: View {
@State private var offset  = 0.0
// 1
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
@State var animationDuration: Double = 5.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onTapGesture {
// 2
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
// 3
animationDuration -= 0.1
if animationDuration <= 0.0 {
timer.upstream.connect().cancel()
} else {
withAnimation(.easeIn) {
self.offset += Double(-UIScreen.main.bounds.size.height + 300)/50
}
}
}
}
}
  1. 这会创建一个计时器发布器,要求计时器每隔0.1秒启动一次。它说计时器应该在main线程上运行,它应该在common运行循环上运行,这是你大部分时间都想使用的循环。(运行循环可以让iOS在用户积极做某事时处理正在运行的代码,例如滚动列表或点击按钮。(

  2. 当您点击圆圈时,计时器会自动取消。

  3. 我们需要使用一个名为onReceive()的新修饰符手动捕获公告(发布(。这接受一个发布者作为其第一个参数(在这个例子中,它是我们的计时器(,并接受一个函数作为其第二个参数运行,它将确保每当发布者发送更改通知时(在这种情况下,每0.1秒(都会调用该函数。

每隔0.1秒,我们会减少动画的持续时间,当持续时间结束时(持续时间=0.0(,我们会停止计时器,否则我们会继续减少offset

查看触发事件-重复使用定时器,了解有关Timer的更多信息

在经历了很多事情之后,我发现了一些对我有用的东西。至少目前是这样,直到我有时间找到更好的方法。

struct WiggleAnimation<Content: View>: View {
var content: Content
@Binding var animate: Bool
@State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content) {
self.content = content()
self._animate = animate
}
}

使用

@State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)

Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}

你有什么需要改变的?将偏移量作为@Binding,从外部提供

它是如何工作的?这里的.id(animate(修改器不刷新视图,只是用一个新视图替换它,而且由于您将偏移量作为绑定属性,所以替换的视图将具有正确的偏移量。

同样,这可能不是最好的解决方案,但它适用于我的情况。

您需要理解的是,只有已经发生的事情才会被动画化。它只是需要更长的时间来展示。换句话说,当您将偏移设置为某个值时,无论发生什么,它都会朝着该值设置动画。因此,您需要做的是在敲击圆时将偏移设置为零。动画系统足够聪明,可以处理"冲突"的动画并取消前一个动画。

另外,一个快速提示:你可以做var offset: CGFloat = 0.0,这样你就不必在onAppear修改器中投射它

最新更新