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

切换数据源时Swift Chart崩溃:数据量减少触发NaN/无限值错误

问题

采用双视图架构:

  • ContentView 包含urgent和regular两个选项的分段筛选器
  • DetailView 通过SwiftData加载数据,初始化时传入筛选参数并执行@Query查询

切换筛选器时出现异常:

  • 从数据量少的urgent切到数据量多的regular,图表显示正常
  • 从regular切回urgent(数据量减少)时,图表崩溃,报错:Thread 1: Fatal error: Double value cannot be converted to Int because it is either infinite or NaN

复现代码:

import Foundation
import SwiftData

@Model
final class Item {
    var isUrgent: Bool
    var name: String
    var amount: Double
    
    init(isUrgent: Bool, name: String, amount: Double) {
        self.isUrgent = isUrgent
        self.name = name
        self.amount = amount
    }
}


enum Filter: String, CaseIterable {
    case urgent, regular
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @State var selectedFilter = Filter.regular
    
    var body: some View {
        NavigationView {
            VStack {
                Picker("", selection: $selectedFilter) {
                    ForEach(Filter.allCases, id: \.self) { filter in
                        Text(filter.rawValue)
                            .tag(filter)
                    }
                }
                .pickerStyle(.segmented)

                DetailView(isUrgent: selectedFilter == .urgent)
            }
            .padding(.horizontal, 16)
            .animation(.default, value: selectedFilter)
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem = Item(
                isUrgent: Bool.random(),
                name: "Some Item",
                amount: Double.random(in: 1.0...50.0)
            )
            modelContext.insert(newItem)
        }
    }
}

import SwiftUI
import SwiftData
import Charts


struct DetailView: View {
    
    @State var viewModel = DetailViewModel()
    var isUrgent: Bool
    @Query private var items: [Item]
    
    init(isUrgent: Bool) {
        self.isUrgent = isUrgent
        self._items = Query(filter: #Predicate { $0.isUrgent == isUrgent } )
    }
    
    var body: some View {
        VStack {
            Chart(viewModel.chartItems, id: \.self) { item in
                SectorMark(angle: .value("amount", item), innerRadius: .ratio(0.65), angularInset: 3.0)
            }
        }
        .task(id: isUrgent) {
            viewModel.setupData(items: items)
        }
    }
}

@Observable
final class DetailViewModel {
 
    var chartItems: [Double] = []
    
    func setupData(items: [Item]) {
        chartItems = items.map { $0.amount }
    }
}

问题原因

崩溃核心是视图生命周期与@Query数据更新不同步:

  • 切换筛选器时,DetailView的@Query会重新加载数据,但.task(id: isUrgent)执行时机晚于图表重渲染
  • 切回urgent时,图表先尝试用旧的chartItems(可能包含失效引用或计算出NaN/无穷值)渲染,此时新数据未加载完成,导致SectorMark计算角度出错
  • 同时,@State var viewModel的初始化方式会保留旧实例,导致数据更新不及时

修复方案

1. 修正ViewModel绑定方式

将@State改为@StateObject,确保视图重建时ViewModel实例正确更新,同时监听items变化而非isUrgent,保证数据同步:

struct DetailView: View {
    @StateObject var viewModel = DetailViewModel()
    var isUrgent: Bool
    @Query private var items: [Item]
    
    init(isUrgent: Bool) {
        self.isUrgent = isUrgent
        self._items = Query(filter: #Predicate { $0.isUrgent == isUrgent } )
    }
    
    var body: some View {
        VStack {
            Chart(viewModel.chartItems, id: \.self) { item in
                SectorMark(angle: .value("amount", item), innerRadius: .ratio(0.65), angularInset: 3.0)
            }
        }
        .onChange(of: items) { newItems in
            viewModel.setupData(items: newItems)
        }
    }
}

2. 过滤无效数据

在ViewModel中过滤掉NaN、无穷大或小于等于0的amount值,避免饼图计算异常:

@Observable
final class DetailViewModel {
    var chartItems: [Double] = []
    
    func setupData(items: [Item]) {
        chartItems = items.compactMap { item in
            let amount = item.amount
            guard !amount.isNaN, !amount.isInfinite, amount > 0 else {
                return nil
            }
            return amount
        }
    }
}

3. 处理空数据场景

当chartItems为空时显示占位视图,避免无数据时崩溃:

struct DetailView: View {
    @StateObject var viewModel = DetailViewModel()
    var isUrgent: Bool
    @Query private var items: [Item]
    
    init(isUrgent: Bool) {
        self.isUrgent = isUrgent
        self._items = Query(filter: #Predicate { $0.isUrgent == isUrgent } )
    }
    
    var body: some View {
        VStack {
            if viewModel.chartItems.isEmpty {
                Text("暂无数据")
                    .font(.title)
                    .foregroundColor(.gray)
            } else {
                Chart(viewModel.chartItems, id: \.self) { item in
                    SectorMark(angle: .value("amount", item), innerRadius: .ratio(0.65), angularInset: 3.0)
                }
            }
        }
        .onChange(of: items) { newItems in
            viewModel.setupData(items: newItems)
        }
    }
}

4. 优化动画(可选)

将动画仅作用于Picker,避免加剧视图更新不同步问题:

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @State var selectedFilter = Filter.regular
    
    var body: some View {
        NavigationView {
            VStack {
                Picker("", selection: $selectedFilter) {
                    ForEach(Filter.allCases, id: \.self) { filter in
                        Text(filter.rawValue)
                            .tag(filter)
                    }
                }
                .pickerStyle(.segmented)
                .animation(.default, value: selectedFilter)

                DetailView(isUrgent: selectedFilter == .urgent)
            }
            .padding(.horizontal, 16)
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem = Item(
                isUrgent: Bool.random(),
                name: "Some Item",
                amount: Double.random(in: 1.0...50.0)
            )
            modelContext.insert(newItem)
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 15:57:04