切换数据源时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
相关产品推荐
相关产品推荐

