I've encountered an animation glitch affecting the text for AxisMarks
and AxisLabels
in the SwiftUI Chart framework. Below are the sample project code and video for reference:
struct ContentView: View {
@State var isDisplayingAge = true
var body: some View {
ChartView(data: ChartDataSample, isDisplayingAge: isDisplayingAge)
.onTapGesture {
withAnimation {
isDisplayingAge.toggle()
}
}
}
}
struct ChartData: Identifiable {
let age: Int
let presence: Int
let grade: Int
let id = UUID()
}
struct ChartView: View {
let data: [ChartData]
let isDisplayingAge: Bool
let xAxis = [0, 50, 100]
var body: some View {
VStack {
Chart {
ForEach(data) { chartData in
BarMark(x: .value("X Axis", isDisplayingAge ? chartData.age : chartData.presence),
y: .value("Y Axis", chartData.grade))
}
}
.chartXAxis(content: {
AxisMarks(position: .bottom, values: xAxis.map{$0}) { data in
AxisValueLabel(String(xAxis[data.index]) + (isDisplayingAge ? " years" : " days"))
}
})
.chartXAxisLabel(content: {
Text(isDisplayingAge ? "Data: age" : "Data: presence")
})
}
.padding(.horizontal, 13)
}
}
As you can observe, when I tap the chart to trigger a content change with animation, the text for AxisMarks
(e.g., x years / x days) and AxisLabels
(e.g., Data: age / Data: presence) animates unusually. A portion of the text is temporarily replaced by an ellipsis ("...") during the animation.
Interestingly, this issue occurs only in one direction of the animation. When transitioning from "year" to "day" or from "presence" to "age," the text simply gets replaced without any noticeable animation.
Can anyone explain what's happening here? Ideally, I'd like the texts to either fade into each other or be replaced without any animation.
Thank you.
This is because, when you are transitioning from a short label to a long label, it tries to fit the long label in the same space. Since it doesn't fit, it truncates it.
To fix, you probably need to deliver the labels yourself and then make sure they don't get truncated. For example:
AxisValueLabel() {
Text(String(xAxis[data.index]) + (isDisplayingAge ? " years" : " days"))
.frame(minWidth: 100, alignment: .leading)
}
Alternatively, if you don't want to set a fixed minimum width, you could use a ZStack
and update visibility using .opacity
:
AxisValueLabel() {
ZStack(alignment: .leading) {
Text(String(xAxis[data.index]) + " years").opacity(isDisplayingAge ? 1 : 0)
Text(String(xAxis[data.index]) + " days").opacity(isDisplayingAge ? 0 : 1)
}
}
Simply setting .fixedSize()
on the Text
also works, but there is some unexpected movement during animation which you probably don't want to see.
You can use the same technique(s) for the X-axis labels too.