iOS 19.2中List嵌套PhaseAnimator结合@Observable引发AttributeGraph循环崩溃
解决iOS 19.2中List编辑时PhaseAnimator引发的AttributeGraph循环崩溃问题
你遇到的问题本质是SwiftUI的AttributeGraph在List编辑(删除/重排)时,与PhaseAnimator的动画更新周期形成了循环依赖:PhaseAnimator的trigger绑定了viewModel.isOverdue,而List编辑会触发视图布局重建,反过来触发PhaseAnimator的状态更新,最终导致循环引用和崩溃。以下是几个实用的解耦方案:
方案1:用独立状态控制器中转动画触发信号
把动画的触发逻辑从TaskRow的viewModel中抽离,通过一个独立的ObservableObject管理动画状态,避免直接绑定viewModel属性引发循环:
class PulseController: ObservableObject { @Published var isAnimating = false func updateAnimationState(_ active: Bool) { guard isAnimating != active else { return } isAnimating = active } } struct TaskRow: View { @Bindable var viewModel: TaskItem @StateObject private var pulseController = PulseController() var body: some View { HStack { Text(viewModel.title) Spacer() Image(systemName: "clock.fill") .phaseAnimator([0.5, 1.0], trigger: pulseController.isAnimating) { content, phase in content.opacity(phase) } } .onAppear { pulseController.updateAnimationState(viewModel.isOverdue) } .onChange(of: viewModel.isOverdue) { newValue in pulseController.updateAnimationState(newValue) } } }
这个方案保留了PhaseAnimator的原生用法,通过中间层切断了viewModel与动画器的直接绑定,避免List编辑时的循环触发。
方案2:编辑时临时禁用动画
通过监听List的编辑状态,在编辑模式激活时暂停动画,编辑结束后恢复,直接切断编辑流程与动画周期的冲突:
// 父视图 struct TaskListView: View { @State private var editMode: EditMode = .inactive @State private var tasks: [TaskItem] = [] var body: some View { List { ForEach(tasks) { task in TaskRow(viewModel: task, isEditing: editMode.isEditing) } .onDelete(perform: deleteTask) .onMove(perform: moveTask) } .environment(\.editMode, $editMode) } private func deleteTask(at offsets: IndexSet) { tasks.remove(atOffsets: offsets) } private func moveTask(from source: IndexSet, to destination: Int) { tasks.move(fromOffsets: source, toOffset: destination) } } // TaskRow修改 struct TaskRow: View { @Bindable var viewModel: TaskItem let isEditing: Bool var body: some View { HStack { Text(viewModel.title) Spacer() Image(systemName: "clock.fill") .phaseAnimator([0.5, 1.0], trigger: viewModel.isOverdue) { content, phase in // 编辑时固定透明度,跳过动画 content.opacity(isEditing ? 1.0 : phase) } // 编辑时禁用动画驱动 .animation(isEditing ? nil : .easeInOut(duration: 1).repeatForever(), value: viewModel.isOverdue) } } }
这个方案实现简单,适合不需要在编辑状态下显示脉冲动画的场景,直接避免了冲突的根源。
方案3:用UIKit动画替代PhaseAnimator
完全绕过SwiftUI的AttributeGraph依赖系统,用UIKit的Core Animation实现脉冲效果,从根本上解耦视图布局与动画逻辑:
struct PulseIconView: UIViewRepresentable { let systemName: String let isOverdue: Bool func makeUIView(context: Context) -> UIImageView { let imageView = UIImageView(image: UIImage(systemName: systemName)) imageView.contentMode = .scaleAspectFit return imageView } func updateUIView(_ uiView: UIImageView, context: Context) { // 先清除现有动画,避免重复添加 uiView.layer.removeAllAnimations() guard isOverdue else { return } let pulseAnim = CABasicAnimation(keyPath: "opacity") pulseAnim.fromValue = 0.5 pulseAnim.toValue = 1.0 pulseAnim.duration = 1.0 pulseAnim.repeatCount = .infinity pulseAnim.autoreverses = true uiView.layer.add(pulseAnim, forKey: "pulseAnimation") } } // 修改后的TaskRow struct TaskRow: View { @Bindable var viewModel: TaskItem var body: some View { HStack { Text(viewModel.title) Spacer() PulseIconView(systemName: "clock.fill", isOverdue: viewModel.isOverdue) .frame(width: 20, height: 20) } } }
这个方案稳定性最高,因为UIKit动画的生命周期完全独立于SwiftUI的视图更新流程,不会触发AttributeGraph的循环问题,适合对动画稳定性要求高的场景。
内容的提问来源于stack exchange,提问作者unknown
相关产品推荐
相关产品推荐

