Charts
Adding advanced visualisations to your apps just became a lot easier thanks to the new Charts library. Lets get to grips with it and look at how we can create beautiful animated charts with minimal efforts.
The basics
First, we need some data to map. I'm going to get started with a simple run model today, which we'll use to visualise our running distance.
struct Run: Identifiable, Hashable {
var id = UUID()
var distanceKm: Double
var day: String
internal init(id: UUID = UUID(), distanceKm: Double, day: String) {
self.id = id
self.distanceKm = distanceKm
self.day = day
}
}
struct Run: Identifiable, Hashable {
var id = UUID()
var distanceKm: Double
var day: String
internal init(id: UUID = UUID(), distanceKm: Double, day: String) {
self.id = id
self.distanceKm = distanceKm
self.day = day
}
}
Next, lets meet Chart
.
As we've come to expect from SwiftUI, this is a really simple construct with some powerful options. Chart
has an initilaiser that takes a trailing closure to build up its content. Lets add one without any content for now.
struct WeeklyDistanceView: View {
@State var data: [Run] = [
.init(distanceKm: 12.5, day: "Monday"),
.init(distanceKm: 6.7, day: "Tuesday"),
.init(distanceKm: 0, day: "Wednesday"),
.init(distanceKm: 21.2, day: "Thursday"),
.init(distanceKm: 10.4, day: "Friday"),
.init(distanceKm: 4.3, day: "Saturday"),
.init(distanceKm: 36.5, day: "Sunday"),
]
var body: some View {
Chart {
// todo
}
}
}
struct WeeklyDistanceView: View {
@State var data: [Run] = [
.init(distanceKm: 12.5, day: "Monday"),
.init(distanceKm: 6.7, day: "Tuesday"),
.init(distanceKm: 0, day: "Wednesday"),
.init(distanceKm: 21.2, day: "Thursday"),
.init(distanceKm: 10.4, day: "Friday"),
.init(distanceKm: 4.3, day: "Saturday"),
.init(distanceKm: 36.5, day: "Sunday"),
]
var body: some View {
Chart {
// todo
}
}
}
Data is added using Mark
classes, which let you decide how you want to plot your data. To get started, we'll use BarMark
to make a simple bar chart. This takes two arguments, the x and y value we want to plot. We're going to have our days along the bottom, and our distance along the side, so thats day for x and distance for y.
var body: some View {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
}
}
var body: some View {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
}
}
The chart we get as a result takes up all the available space, and nicely plots out our values for us. It's also pink.
There's more options for us if we want them, including LineMark
, AreaMark
, PointMark
and RectangleMark
. Here's what they all look like.
This shows how easy it is to re-use the same datasource for a whole bunch of different charts.
Getting fancy
Chart aren't limited to just one type, which means you can stack them atop one another.
For example, I might want to overlay my run data over the top of my sleep data, to check that im resting proportionally in-line with my effort levels.
To do this, you can simply stack content inside your Chart
.
Here, i've added a sleepHours
property to my run object, and used that to add a second chart over the top of my running distance.
var body: some View {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
ForEach(data) { run in
AreaMark(
x: .value("Date", run.day),
y: .value("Distance", run.sleepHours)
)
.foregroundStyle(.blue.opacity(0.5))
}
}
.frame(height: 200)
}
var body: some View {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
ForEach(data) { run in
AreaMark(
x: .value("Date", run.day),
y: .value("Distance", run.sleepHours)
)
.foregroundStyle(.blue.opacity(0.5))
}
}
.frame(height: 200)
}
If the marks are the same, you get some special interactions between them. Here's the same example with two BarMarks, and you can see how it stacks nicely.
Animating charts comes for free when you animate the data change, so lets take a look at how that could work.
Here I've got a chart of runs, and when I tap the plus button it adds another one. I can use implicit animations to tell SwiftUI I want any changes to animate
struct AnimatedChartsView: View {
@State var data: [Run] = [
.init(distanceKm: 12.5, day: "Monday"),
.init(distanceKm: 6.7, day: "Tuesday"),
.init(distanceKm: 0, day: "Wednesday"),
.init(distanceKm: 21.2, day: "Thursday"),
.init(distanceKm: 10.4, day: "Friday"),
.init(distanceKm: 4.3, day: "Saturday")
]
var body: some View {
VStack {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
}
.animation(.spring, value: data)
.frame(height: 200)
Button(action: {
data.append(.init(distanceKm: Double.random(in: 0..<40), day: "Sunday"))
}) {
Label("Add your run", systemImage: "plus")
}
}
}
}
struct AnimatedChartsView: View {
@State var data: [Run] = [
.init(distanceKm: 12.5, day: "Monday"),
.init(distanceKm: 6.7, day: "Tuesday"),
.init(distanceKm: 0, day: "Wednesday"),
.init(distanceKm: 21.2, day: "Thursday"),
.init(distanceKm: 10.4, day: "Friday"),
.init(distanceKm: 4.3, day: "Saturday")
]
var body: some View {
VStack {
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.pink)
}
}
.animation(.spring, value: data)
.frame(height: 200)
Button(action: {
data.append(.init(distanceKm: Double.random(in: 0..<40), day: "Sunday"))
}) {
Label("Add your run", systemImage: "plus")
}
}
}
}
Here's how that feels.
Notice what happens when I tap add repeatedly? Instead of adding new elements, SwiftUI is smart enough to manage this for me, and just add the new value to the existing "Sunday" rather than adding a new element along the bottom.
Making them pretty
This is an entirely optional step, but I think it shows how far you can go with these in a very short space of time.
A simple way to improve the visuals here is to wrap the charts in some nice boxes, give them some titles, and customise the colors a little, like this.
var lineChart: some View {
VStack(alignment: .leading) {
Text("Lines")
.font(.subheadline.weight(.semibold))
Chart {
ForEach(dataTwo) { (run) in
LineMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.blue)
}
}
.frame(height: 200)
}
}
var lineChart: some View {
VStack(alignment: .leading) {
Text("Lines")
.font(.subheadline.weight(.semibold))
Chart {
ForEach(dataTwo) { (run) in
LineMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(.blue)
}
}
.frame(height: 200)
}
}
We've not done much, but this makes a big difference, espeically if you add lots of these together to make a big data view.
And just for fun, lets randomise them with an animation.
Finally, lets add a little more flair by using some gradients. We can use anything that you can use in foregroundStyle
, so we can easily apply some lovely gradients.
var barChart: some View {
VStack(alignment: .leading) {
Text("Bars")
.font(.subheadline.weight(.semibold))
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(
.linearGradient(
colors: [.blue, .teal],
startPoint: .top,
endPoint: .bottom
)
)
}
}
.frame(height: 200)
}
}
var barChart: some View {
VStack(alignment: .leading) {
Text("Bars")
.font(.subheadline.weight(.semibold))
Chart {
ForEach(data) { (run) in
BarMark(
x: .value("Date", run.day),
y: .value("Distance", run.distanceKm)
)
.foregroundStyle(
.linearGradient(
colors: [.blue, .teal],
startPoint: .top,
endPoint: .bottom
)
)
}
}
.frame(height: 200)
}
}
Thanks for reading!
You can find my code on github. The good stuff is in WeeklyChartsView
.