SwiftUI:如何制作旋转刷新按钮动画

问题描述 投票:0回答:1

我想要一个行为如下的“刷新”按钮:

  • 只要数据正在加载,它就应该旋转
  • 至少需要旋转一整圈
  • 加载完成后应完成当前旋转(不要中途停止或旋转回来)

我通过计算延迟、使用动画专用的

@State
并在再次关闭动画之前使用
Task.sleep(...)
来实现它的功能。在我看来,由于多种原因,这是一个糟糕的解决方案:

  • 不靠谱
  • 由于时间计算不准确,动画会旋转回来
  • 它试图通过不精确的日期计算来计时动画
  • ...

这是我当前的(糟糕的)解决方案:

struct LoadingView: View {
    @State var isLoadingData = false
    @State var isAnimating = false
    @State var lastRefreshStarted: Date?

    var body: some View {
        Button {
            handleRefresh()
        } label: {
            Image(systemName: "arrow.triangle.2.circlepath")
                .rotationEffect(isAnimating ? .degrees(180) : .degrees(0))
                .animation(
                    isAnimating ? Animation.linear(duration: 1).repeatForever(autoreverses: false) :
                        .default, value: isAnimating)
        }
        .onChange(of: isLoadingData) {
            handleIsLoadingDataChange()
        }
    }
    
    func handleRefresh() {
        if isAnimating { return }
        Task {
            isLoadingData = true
            lastRefreshStarted = .now
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            // loading for an unknown time here
            await MainActor.run {
                isLoadingData = false
            }
        }
    }
    
    func handleIsLoadingDataChange() {
        let newValue = isLoadingData
        if newValue {
            // start the animation as soon as data is loading
            withAnimation {
                isAnimating = newValue
            }
        } else {
            // if data is done loading, set a delay to fill up the next full second
            let elapsedTime = Date().timeIntervalSince(lastRefreshStarted ?? Date())
            let remainder = elapsedTime.truncatingRemainder(dividingBy: 1)
            // add 0.1s buffer as calculation is not precise and it's better to spin back slightly
            // than spin back all the way to the previous rotation
            let delay = (1 - remainder) + 0.1
            Task {
                await Task.sleep(s: delay)
                await MainActor.run {
                    withAnimation {
                        isAnimating = isLoadingData
                    }
                }
            }
        }
    }
}

这样做的正确方法是什么?

swift animation swiftui
1个回答
0
投票

感谢@Benzy Neez 和这个答案,这解决了我遇到的问题:

它不是永远旋转并尝试在正确的时间停止,而是旋转 180 度并在动画完成后将其延伸,但

isLoadingData
仍然如此。

struct LoadingView: View {
    @State var isLoadingData = false
    @State private var rotation = 0.0

    private func nextTurn() {
        withAnimation(.linear(duration: 1)) {
            rotation += 180
        }
    }

    var body: some View {
        Button {
            handleRefresh()
        } label: {
            Image(systemName: "arrow.triangle.2.circlepath")
                .rotationEffect(Angle(degrees: rotation))
                .onChange(of: isLoadingData) {
                    if isLoadingData {
                        nextTurn()
                    }
                }
                .modifier(AnimationCompletionCallback(animatedValue: rotation) {
                    if isLoadingData {
                        nextTurn()
                    }
                })
        }
    }

    func handleRefresh() {
        if isLoadingData { return }
        Task {
            isLoadingData = true
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            // loading for an unknown time here
            await MainActor.run {
                isLoadingData = false
            }
        }
    }
}
struct AnimationCompletionCallback: AnimatableModifier {
   var targetValue: Double
   var completion: () -> Void

   init(animatedValue: Double, completion: @escaping () -> Void) {
       self.targetValue = animatedValue
       self.completion = completion
       self.animatableData = animatedValue
   }

   var animatableData: Double {
       didSet {
           checkIfFinished()
       }
   }

   func checkIfFinished() {
       if animatableData == targetValue {
           DispatchQueue.main.async {
               self.completion()
           }
       }
   }

   func body(content: Content) -> some View {
       content
   }
}
© www.soinside.com 2019 - 2024. All rights reserved.