SwiftUI — Charts
Apple is continuously adding new features and frameworks to help developers build delightful experiences for their users and does it quickly in SwiftUI.
The Charts are very useful to users compared to rows and columns of lots of data.
Charts make complex data simple and easy to understand.
In SwiiftUI, Swift Charts is a flexible framework that allows you to create charts using declarative syntax.
Previously if have to create charts then we have to create either from scratch or we have to use 3rd party libraries. But now that headache is no more thanks to Swift Charts.
Swift charts include the following marks by default:
BarMark
PointMark
LineMark
AreaMark
RuleMark
RectangleMark
We can create custom marks, as Marks are extensible.
In this story, we are not going to make it boring by discussing only theory.
Let’s create something. We are going to create a part of ExpenseTracker app.
Consider, you are creating an app to track your day-to-day expenses. Normally we show the records in rows/columns, but charts will be very easy to see how much the user had done expenses in each month.
It provides an attractive UI/UX feel to your app.
This is how the end result will look like. In this story we are going to implement BarMark
, LineMark
, AreaMark
and RectangleMark
.
Let’s Create a new swiftUI App.
before we write any code related to chart, we need to understand 1 thing. without sufficient data, charts don’t look good.
So in this app, we are going to create charts which shows monthly expenses of user.
Model
First we are going to create Expense model.
struct Expense: Identifiable {
var date: Date
var type: String
var amount: Double
var id = UUID()
}
This is our model. It contains date, expense type and expense amount.
View Model
Now, we are going to create ExpenseVM which contains below code.
class ExpenseVM {
// 1
static func getDummyExpenses() -> [Expense] {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "M/d/yyyy"
let data: [Expense] = [
.init(date: dateFormatter.date(from: "12/1/2022") ?? Date(), type: "Food", amount: 55.0),
.init(date: dateFormatter.date(from: "12/1/2022") ?? Date(), type: "Shopping", amount: 44.0),
.init(date: dateFormatter.date(from: "12/1/2022") ?? Date(), type: "Movie", amount: 24.0),
.init(date: dateFormatter.date(from: "11/1/2023") ?? Date(), type: "Food", amount: 103.0),
.init(date: dateFormatter.date(from: "11/4/2023") ?? Date(), type: "Movie", amount: 76.0),
.init(date: dateFormatter.date(from: "10/1/2023") ?? Date(), type: "Food", amount: 103.0),
.init(date: dateFormatter.date(from: "10/2/2023") ?? Date(), type: "Food", amount: 10.0),
.init(date: dateFormatter.date(from: "10/3/2023") ?? Date(), type: "Food", amount: 30.0),
.init(date: dateFormatter.date(from: "9/4/2023") ?? Date(), type: "Movie", amount: 85.0),
.init(date: dateFormatter.date(from: "9/6/2023") ?? Date(), type: "Movie", amount: 12.0),
.init(date: dateFormatter.date(from: "9/17/2023") ?? Date(), type: "Movie", amount: 50.0),
.init(date: dateFormatter.date(from: "8/1/2023") ?? Date(), type: "Food", amount: 10.0),
.init(date: dateFormatter.date(from: "8/6/2023") ?? Date(), type: "Food", amount: 40.0),
.init(date: dateFormatter.date(from: "8/9/2023") ?? Date(), type: "Food", amount: 80.0),
.init(date: dateFormatter.date(from: "7/4/2023") ?? Date(), type: "Movie", amount: 20.0),
.init(date: dateFormatter.date(from: "7/24/2023") ?? Date(), type: "Movie", amount: 60.0),
.init(date: dateFormatter.date(from: "7/14/2023") ?? Date(), type: "Movie", amount: 20.0),
.init(date: dateFormatter.date(from: "6/1/2023") ?? Date(), type: "Food", amount: 103.0),
.init(date: dateFormatter.date(from: "6/12/2023") ?? Date(), type: "Food", amount: 10.0),
.init(date: dateFormatter.date(from: "6/15/2023") ?? Date(), type: "Food", amount: 56.0),
.init(date: dateFormatter.date(from: "5/4/2023") ?? Date(), type: "Movie", amount: 30.0),
.init(date: dateFormatter.date(from: "5/6/2023") ?? Date(), type: "Movie", amount: 50.0),
.init(date: dateFormatter.date(from: "5/9/2023") ?? Date(), type: "Movie", amount: 10.0),
.init(date: dateFormatter.date(from: "5/14/2023") ?? Date(), type: "Movie", amount: 80.0),
.init(date: dateFormatter.date(from: "5/3/2023") ?? Date(), type: "Movie", amount: 70.0),
.init(date: dateFormatter.date(from: "4/1/2023") ?? Date(), type: "Food", amount: 103.0),
.init(date: dateFormatter.date(from: "4/4/2023") ?? Date(), type: "Food", amount: 10.0),
.init(date: dateFormatter.date(from: "4/7/2023") ?? Date(), type: "Food", amount: 50.0),
.init(date: dateFormatter.date(from: "3/4/2023") ?? Date(), type: "Movie", amount: 40.0),
.init(date: dateFormatter.date(from: "3/5/2023") ?? Date(), type: "Movie", amount: 12.0),
.init(date: dateFormatter.date(from: "3/6/2023") ?? Date(), type: "Movie", amount: 87.0),
.init(date: dateFormatter.date(from: "2/4/2023") ?? Date(), type: "Movie", amount: 95.0),
.init(date: dateFormatter.date(from: "2/5/2023") ?? Date(), type: "Movie", amount: 15.0),
.init(date: dateFormatter.date(from: "2/6/2023") ?? Date(), type: "Movie", amount: 15.0),
.init(date: dateFormatter.date(from: "1/4/2023") ?? Date(), type: "Movie", amount: 44.0),
.init(date: dateFormatter.date(from: "1/5/2023") ?? Date(), type: "Movie", amount: 84.0),
.init(date: dateFormatter.date(from: "1/6/2023") ?? Date(), type: "Movie", amount: 44.0)
]
return data
}
// 2.
static func expensesByMonth(_ month: Int) -> [Expense] {
return ExpenseVM.getDummyExpenses().filter {
Calendar.current.component(.month, from: $0.date) == month + 1
}
}
}
- We created a static function getDummyExpenses() which will create and return dummy Expense data.
- We create second static function which return expenses by month.
Make sure to write “import Charts” Where you want to use charts in SwiftUI project.
Bar Chart
Lets write code for showing Bar Chart for Expense.
// 1
struct ExpenseBarChart: View {
// 2
var expencesList : [Expense]
// 3
var body: some View {
// 4
Chart(0..<12, id: \.self) { month in
// 5
let expenseValue = sumOfExpensesIn(month)
let monthName = DateUtils.monthAbbreviationFromInt(month)
// 6
BarMark(
x: .value("Expenses", expenseValue),
y: .value("Month", monthName)
)
// 7
.foregroundStyle(.mint)
// 8
.annotation(position: .trailing) {
Text(String(format: "%.2f $", expenseValue))
.font(.caption)
}
.accessibilityLabel(DateUtils.monthFromInt(month))
.accessibilityValue("Expense \(expenseValue) $")
}
// 9
.chartXAxisLabel("Months", position: .leading)
}
func sumOfExpensesIn(_ month: Int) -> Double {
self.expencesList.filter {
Calendar.current.component(.month, from: $0.date) == month + 1
}
.reduce(0) { $0 + $1.amount }
}
}
enum DateUtils {
static func monthFromInt(_ month: Int) -> String {
let monthSymbols = Calendar.current.monthSymbols
return monthSymbols[month]
}
static func monthAbbreviationFromInt(_ month: Int) -> String {
let monthSymbols = Calendar.current.shortMonthSymbols
return monthSymbols[month]
}
}
- We are going to create struct ExpenseBarChart view.
- It will be having a property expencesList which will contain array of Expense.
- We are going to create body for view.
- Add a
ForEach
loop to generate a bar chart for each month. - Use
sumOfExpensesIn(_ month:)
Get the sum of the expenses data for the month. Get the abbreviated month name by passing the month number tomonthAbbreviationFromInt(_:)
fromDateUtils
. - Create BarMark where X-axis for Expense and Y-axis for Month.
- Apply .mint color to Bar.
- Add annotation to end of bar, which will show the total expenses done in month.
- Set chartXAxisLabel as Months.
how it looks :
Look nice, right ? Moving to Next type of chart.
Line Mark
Lets show monthly average expenses with LineMark
lets write below code for that:
// 1
var monthlyAvgExpenseView: some View {
// 2
List(0..<12) { month in
// 3
VStack {
// 4
Chart( ExpenseVM.expensesByMonth(month)) { dayInfo in
// 5
LineMark(
x: .value("Day", dayInfo.date),
y: .value("Expenses", dayInfo.amount)
)
// 6
.foregroundStyle(.orange)
// 7
.interpolationMethod(.catmullRom)
}
Text(Calendar.current.monthSymbols[month])
}
.frame(height: 150)
}
.listStyle(.plain)
}
- Creating monthlyAvgExpenseView.
- Add a
ForEach
loop to generate a Line chart for each month avg expenses. - Add VStack.
- Create chart for average expense of month.
- You use
LineMark
to create a line chart. For each day within the month, you add aLineMark
. The x-axis indicates the day and the y-axis the day's average expenses. - Set the color of the line chart to orange using
.foregroundStyle.
- To smooth the rendered line, you use
.interpolationMethod
and call a Catmull-Rom spline to interpolate the data points.
Rectangle Mark
Lets show monthly average expense with Rectangle Mark.
// 1
var monthlyAvgExpenseViewInRectangleMark: some View {
// 2
List(0..<12) { month in
// 3
VStack {
// 4
Chart( ExpenseVM.expensesByMonth(month)) { dayInfo in
// 5
RectangleMark(
x: .value("Day", dayInfo.date),
y: .value("Expenses", dayInfo.amount),
width: 5,
height: 25
)
// 6
.foregroundStyle(.purple)
// 7
.interpolationMethod(.catmullRom)
}
Text(Calendar.current.monthSymbols[month])
}
.frame(height: 150)
}
.listStyle(.plain)
}
5. Only Change in this point o f code. For Rectangle Mark, It contains 4 parameters, X, y and width and height. This is the end result.
Area Chart
Area chart show a diff type of chart. Here I tried this to show monthly average expense using area chart.
Here is the code:
// 1
var monthlyAvgExpenseViewInAreaMark: some View {
// 2
List(0..<12) { month in
// 3
VStack {
// 4
Chart( ExpenseVM.expensesByMonth(month)) { dayInfo in
// 5
AreaMark(
x: .value("Day", dayInfo.date),
y: .value("Expenses", dayInfo.amount)
)
// 6
.foregroundStyle(.red)
// 7
.interpolationMethod(.catmullRom)
}
Text(Calendar.current.monthSymbols[month])
}
.frame(height: 150)
}
.listStyle(.plain)
}
Here is the ContentView code. I added list and as per destination value I open diff charts.
struct ContentView: View {
private var chartTypes: [String] = [ "Bar Chart", "Line Chart", "Rectangle Chart", "Area Chart" ]
var body: some View {
NavigationStack {
List(chartTypes, id: \.self) { chartType in
NavigationLink(value: chartType) {
Text(chartType)
}
}
.listStyle(.plain)
.navigationDestination(for: String.self) { chartType in
if chartType == "Bar Chart" {
ExpenseBarChart(expencesList: ExpenseVM.getDummyExpenses())
} else if chartType == "Line Chart" {
monthlyAvgExpenseView
} else if chartType == "Rectangle Chart" {
monthlyAvgExpenseViewInRectangleMark
} else {
monthlyAvgExpenseViewInAreaMark
}
}
.navigationTitle("Charts")
}
}
}
This result is quite clean.
That’s it. As you can see with SwiftUI and Charts, It make it very easy to convert data into charts.
Here is complete Source-Code.
Those who love to explore SwiftUI, I definitely recommend them to try this Chart framework. Thanks for reading.