为何SwiftUI Chart无法生成并列分组柱状图?
分组柱状图无法并列显示的原因分析与修复方案
问题场景
在iOS 16及以上版本的SwiftUI中,尝试通过position(by:)修饰符实现Burned和Consumed两类柱状图的并列分组显示,但最终两类柱状图在同一日期位置重叠,未达到预期的分组效果。
原代码
enum SwiftChartEnergyType { case Burned case Consumed } struct SwiftChartEnergyObject: Identifiable { let id = UUID() let energy: Int let date: Date var swiftChartEnergyType: SwiftChartEnergyType } @available(iOS 16.0, *) struct WeeklyEnergyGroupSWIFTBarChart: View { var energyDays: [EnergyDay] = [] var dietaryDays: [DietaryDay] = [] var body: some View { VStack { Chart(prepareDatForChart(), id: \.energyType) { item in ForEach(item.data) { BarMark( x: .value("Date", $0.date), y: .value("Energy", $0.energy) ) .foregroundStyle(by: .value("Energy Type", item.energyType)) .position(by: .value("Energy Type", item.energyType)) // 未实现并列分组 } } .chartYAxis { AxisMarks(position: .leading) } .chartForegroundStyleScale([ "Burned" : TrackerConstants.AppleFitnessOrange, "Consumed": TrackerConstants.AppleFitnessGreen ]) } } private func prepareDatForChart() -> [(energyType: String, data: [SwiftChartEnergyObject])] { let data = [ (energyType: "Burned", data: energyDays.map { SwiftChartEnergyObject(energy: $0.totalEnergyBurned, date: $0.date, swiftChartEnergyType: .Burned) } ), (energyType: "Consumed", data: dietaryDays.map { SwiftChartEnergyObject(energy: $0.totalCalories, date: $0.date, swiftChartEnergyType: .Consumed) } ) ] return data } }
原因排查
数据结构嵌套导致Chart无法识别分组关联
原代码将数据按类型拆分为两个分组的元组数组,外层Chart遍历分组,内层ForEach遍历每个分组的数据。这种嵌套结构下,Chart会将每个分组的BarMark视为独立的数据集,无法识别同一日期下的两个数据点属于同一分组单元,因此无法触发position(by:)的分组偏移逻辑。position(by:)的使用层级错误position(by:)需要作用在同一层级的BarMark上,让Chart能针对同一X轴值(日期)下的不同类别数据进行位置计算。但原代码的嵌套ForEach使得每个BarMark属于不同的分组上下文,Chart无法将它们关联为同一日期下的分组元素,修饰符无法生效。
修复方案
步骤1:重构数据源为扁平数组
将两类数据合并为一个扁平的SwiftChartEnergyObject数组,确保同一日期的两类数据都在同一个数据集里,让Chart能一次性遍历所有数据点。
步骤2:调整Chart结构
直接遍历扁平后的数组,移除嵌套的ForEach,让每个BarMark处于同一层级,同时正确使用position(by:)指定类别字段。
修改后的代码
enum SwiftChartEnergyType: String { // 改为String类型,方便直接作为图例文本 case Burned = "Burned" case Consumed = "Consumed" } struct SwiftChartEnergyObject: Identifiable { let id = UUID() let energy: Int let date: Date var swiftChartEnergyType: SwiftChartEnergyType } @available(iOS 16.0, *) struct WeeklyEnergyGroupSWIFTBarChart: View { var energyDays: [EnergyDay] = [] var dietaryDays: [DietaryDay] = [] var body: some View { VStack { Chart(prepareDatForChart()) { item in BarMark( x: .value("Date", item.date), y: .value("Energy", item.energy) ) .foregroundStyle(by: .value("Energy Type", item.swiftChartEnergyType.rawValue)) .position(by: .value("Energy Type", item.swiftChartEnergyType.rawValue)) // 同一层级下生效 } .chartYAxis { AxisMarks(position: .leading) } .chartForegroundStyleScale([ SwiftChartEnergyType.Burned.rawValue : TrackerConstants.AppleFitnessOrange, SwiftChartEnergyType.Consumed.rawValue: TrackerConstants.AppleFitnessGreen ]) } } private func prepareDatForChart() -> [SwiftChartEnergyObject] { let burnedData = energyDays.map { SwiftChartEnergyObject(energy: $0.totalEnergyBurned, date: $0.date, swiftChartEnergyType: .Burned) } let consumedData = dietaryDays.map { SwiftChartEnergyObject(energy: $0.totalCalories, date: $0.date, swiftChartEnergyType: .Consumed) } // 合并为扁平数组 return burnedData + consumedData } }
关键说明
- 把
SwiftChartEnergyType改为String枚举,直接用rawValue作为图例文本,避免手动拼接字符串。 - 数据源合并为扁平数组后,Chart可以识别同一日期下的两个数据点,
position(by:)会自动计算每个类别柱状图的偏移量,实现并列分组效果。 - 移除嵌套的ForEach,让所有BarMark处于同一层级,确保Chart能正确处理分组逻辑。
内容的提问来源于stack exchange,提问作者GarySabo
相关产品推荐
相关产品推荐

