You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI录制按钮循环动画与状态异常及UIKit转SwiftUI实现

SwiftUI 录制按钮循环扩散动画修复方案

问题概述

现有SwiftUI录制按钮定义了RecordState三种状态:.none(初始)、.recording(录制中)、.notRecording(未录制),当前动画存在以下异常:

  • 从.none切换到.recording时动画正常
  • 切换回.notRecording无过渡动画
  • 再次切换到.recording时,动画出现变形叠加

错误代码示例(常见问题版本)

enum RecordState {
    case none, recording, notRecording
}

struct RecordButton: View {
    @Binding var state: RecordState
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(Color.red)
                .scaleEffect(scale)
                .opacity(1 - (scale - 1) / 2)
            Circle()
                .fill(Color.red)
                .frame(width: 30, height: 30)
        }
        .frame(width: 60, height: 60)
        .onTapGesture {
            switch state {
            case .none, .notRecording:
                state = .recording
                withAnimation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) {
                    scale = 3.0
                }
            case .recording:
                state = .notRecording
                scale = 1.0
            }
        }
    }
}

// 测试视图
struct RecordButtonTestView: View {
    @State private var state: RecordState = .none
    
    var body: some View {
        RecordButton(state: $state)
    }
}

问题根源

  1. 动画与业务状态耦合:直接修改scale属性绑定重复动画,再次触发时旧动画未被正确终止,导致多动画叠加
  2. 无终止过渡:切换到.notRecording时直接重置scale,没有平滑过渡动画
  3. 状态同步缺失:.none状态未同步停止动画,可能遗留动画状态

修复后的SwiftUI代码

enum RecordState {
    case none, recording, notRecording
}

struct RecordButton: View {
    @Binding var state: RecordState
    @State private var isAnimating = false
    
    var body: some View {
        ZStack {
            // 扩散圆环动画层
            Circle()
                .stroke(Color.red, lineWidth: 2)
                .scaleEffect(isAnimating ? 3.0 : 1.0)
                .opacity(isAnimating ? 0 : 1)
                .animation(
                    isAnimating ?
                    Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: false) :
                    .easeOut(duration: 0.3),
                    value: isAnimating
                )
            
            // 中心按钮(区分录制/未录制状态)
            Circle()
                .fill(state == .recording ? Color.red : Color.gray)
                .frame(width: 30, height: 30)
                .overlay(
                    state == .notRecording ?
                    Circle().stroke(Color.white, lineWidth: 2) :
                    nil
                )
        }
        .frame(width: 60, height: 60)
        .onTapGesture {
            switch state {
            case .none, .notRecording:
                state = .recording
                isAnimating = true
            case .recording:
                state = .notRecording
                isAnimating = false
            }
        }
        // 同步none状态与动画状态
        .onChange(of: state) { newState in
            if newState == .none {
                isAnimating = false
            }
        }
    }
}

// 测试视图(含重置按钮)
struct RecordButtonTestView: View {
    @State private var state: RecordState = .none
    
    var body: some View {
        VStack(spacing: 20) {
            RecordButton(state: $state)
            Button("重置为初始状态") {
                state = .none
            }
        }
        .padding()
    }
}

关键修复点

  1. 分离动画与业务状态:用独立的isAnimating变量控制动画启停,避免业务状态与动画逻辑耦合
  2. 精准动画触发:通过value: isAnimating让SwiftUI感知动画状态变化,确保每次切换都能正确启动/终止动画,避免叠加
  3. 添加终止过渡:切换到非录制状态时,扩散圆环通过.easeOut(duration: 0.3)平滑缩回到初始状态
  4. 状态同步:监听state变化,当回到.none时自动停止动画,保证状态一致性

与UIKit实现逻辑对齐

UIKit中通常通过添加/移除CAAnimationGroup实现循环扩散,修复后的SwiftUI方案完全对齐该逻辑:

  • 每次启动动画前自动终止旧动画(SwiftUI通过状态切换管理动画生命周期)
  • 停止时添加平滑过渡动画,而非直接重置属性
  • 分离业务状态与动画控制状态,避免耦合异常

内容的提问来源于stack exchange,提问作者imjohnnotsina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 08:03:29