SwiftUI自定义Activity Indicator特定场景下不动画问题
问题分析与解决方案
一、为什么switch内的ActivityIndicator无动画?
当viewModel.state从.idle切换到.loading时,ActivityIndicator是首次被添加到视图层级的,此时shouldAnimate已经是true(在onAppear中设置)。你的自定义指示器依赖shouldAnimate从false到true的状态变化触发动画,但视图初始化时直接使用true,缺少动画所需的状态变更触发条件。系统ProgressView内部做了特殊处理,初始加载状态也能启动动画,而你的自定义实现没有这个逻辑。
二、为什么外部的指示器无法隐藏?
放在switch外的ActivityIndicator始终存在于视图层级中,进入.dataReady分支时,onAppear设置shouldAnimate = false,但你的动画使用了.repeatCount(.max),切换为false后动画只会完成当前循环才停止,不会立即重置。另外,不带value参数的旧版animation方法在SwiftUI中容易出现动画触发不及时的问题,导致状态更新无法同步到视图。
修复方案
1. 重构自定义ActivityIndicator(适配SwiftUI动画机制)
改用SwiftUI推荐的animation(_:value:)绑定动画触发源,同时添加初始动画触发逻辑:
import SwiftUI struct ActivityIndicator: View { @Binding var shouldAnimate: Bool private let count: Int = 5 private let element = Circle().frame(width: 15, height: 15) public var body: some View { GeometryReader { geometry in ForEach(0..<count, id: \.self) { index in item(forIndex: index, in: geometry.size) .rotationEffect(shouldAnimate ? .degrees(360) : .degrees(0)) .scaleEffect(shouldAnimate ? (CGFloat(index + 1) / CGFloat(count)) : 1) // 绑定value确保状态变化时触发动画 .animation( Animation.timingCurve(0.5, 0.15 + Double(index)/5, 0.25, 1, duration: 2.0) .repeatCount(shouldAnimate ? .max : 1, autoreverses: false), value: shouldAnimate ) .frame(width: geometry.size.width, height: geometry.size.height) } } .aspectRatio(contentMode: .fit) // 初始状态为true时手动触发动画 .onAppear { if shouldAnimate { withAnimation { self.shouldAnimate = true } } } } private func item(forIndex index: Int, in geometrySize: CGSize) -> some View { element .offset(y: geometrySize.width/10 - geometrySize.height/2) } }
2. 优化CustomView的状态控制
用viewModel.state直接控制指示器显示/隐藏,通过onReceive同步shouldAnimate状态,避免依赖onAppear的不确定性:
struct CustomView: View { @ObservedObject var viewModel: EventsListViewModel @State var shouldAnimate: Bool = false var body: some View { VStack { switch viewModel.state { case .idle: Color.clear.onAppear(perform: viewModel.getData) case .loading: ActivityIndicator(shouldAnimate: $shouldAnimate) case .dataReady(let eventsList): EventsListView(eventsList: eventsList, viewModel: viewModel) } } .navigationBarTitle("Back") .onAppear { viewModel.state = .idle } // 监听state变化自动控制动画 .onReceive(viewModel.$state) { state in switch state { case .loading: shouldAnimate = true case .dataReady, .idle: shouldAnimate = false } } } }
3. 额外优化:替换NotificationCenter(可选)
建议用Combine的PassthroughSubject替代NotificationCenter,更符合SwiftUI响应式编程习惯:
class EventsListViewModel: ObservableObject { enum EventStates { case idle case loading case dataReady([EventsModel]) } @Published var state = EventStates.idle private let eventsSubject = PassthroughSubject<[EventsModel], Never>() private var cancellables = Set<AnyCancellable>() init() { eventsSubject .receive(on: DispatchQueue.main) .sink { [weak self] eventsList in self?.state = .dataReady(eventsList) } .store(in: &cancellables) } // 直接发送事件替代通知 func processEventsList(_ eventsList: [EventsModel]) { eventsSubject.send(eventsList) } func getData() { state = .loading // 执行数据请求,完成后调用processEventsList } }
内容的提问来源于stack exchange,提问作者tech_human
相关产品推荐
相关产品推荐

