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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 00:57:13