SwiftUI — Charts

Krupanshu Sharma
7 min readJan 6, 2023

--

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
}
}
}
  1. We created a static function getDummyExpenses() which will create and return dummy Expense data.
  2. 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]
}
}
  1. We are going to create struct ExpenseBarChart view.
  2. It will be having a property expencesList which will contain array of Expense.
  3. We are going to create body for view.
  4. Add a ForEach loop to generate a bar chart for each month.
  5. Use sumOfExpensesIn(_ month:) Get the sum of the expenses data for the month. Get the abbreviated month name by passing the month number to monthAbbreviationFromInt(_:) from DateUtils.
  6. Create BarMark where X-axis for Expense and Y-axis for Month.
  7. Apply .mint color to Bar.
  8. Add annotation to end of bar, which will show the total expenses done in month.
  9. 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)
}
  1. Creating monthlyAvgExpenseView.
  2. Add a ForEach loop to generate a Line chart for each month avg expenses.
  3. Add VStack.
  4. Create chart for average expense of month.
  5. You use LineMark to create a line chart. For each day within the month, you add a LineMark. The x-axis indicates the day and the y-axis the day's average expenses.
  6. Set the color of the line chart to orange using .foregroundStyle.
  7. To smooth the rendered line, you use .interpolationMethod and call a Catmull-Rom spline to interpolate the data points.
Output

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)
}
Output looks like this.

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.

--

--