Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

10. Working With Datasets
Written by Caroline Begbie

Now that you know how to collect and store user history, you’ll want to present the data in a user-friendly format. In this chapter, you’ll learn how to deal with sets of data.

First, you’ll allow the user to modify and delete the history data. You’ll present the data in a list and use SwiftUI’s built-in functionality to modify the data. Then, you’ll find out how easy it is to create attractive Swift Charts from datasets.

➤ Open the starter project for this chapter.

This project is the almost same as the previous chapter’s challenge project with these changes:

  • On first run of the project on Simulator, when there is no history, the app will run HistoryStore.copyHistoryTestData(), in HistoryStoreDevData.swift. This method copies a sample history.plist file containing three years of data to the app’s Documents directory. Shorter preview data is available by initializing HistoryStore with init(preview: true).
  • HistoryView.swift will get more complicated through this chapter, so subviews are now in separate properties:

Initial HistoryView subviews
Initial HistoryView subviews

  • DateExtension.swift and Exercise.swift contain some new supporting code.
  • Assets.xcassets contains some new colors.

➤ In Simulator, choose Device ▸ Erase All Contents and Settings…. Erase all the contents to ensure that you start with no history data.

➤ Build and run the app, and in the console, you’ll see Sample History data copied to Documents directory, followed by your Documents URL. Tap the History button to see the sample data.

Sample data
Sample data

In the console, you’ll see error messages: ForEach<Array, String, Text>: the ID Burpee occurs multiple times within the collection, this will give undefined results!. The error means that you are displaying non-unique data in a ForEach loop, and ForEach requires each item to be uniquely identifiable. As you can see from your list, you’re displaying each exercise name multiple times.

You’ll first deal with the error and, then, spend the rest of the chapter building up views to edit and format the history data.

Accumulating Data

Skills you’ll learn in this section: Sets; badges

Instead of showing all the exercises on each line, you’ll show a list of dates, with the number of times you’ve performed the exercises accumulated within those dates. Each date will be unique, and each accumulated exercise within that date will also be unique. The ForEach loops will then show unique data with no errors.

Swift Dive: Sets

To accumulate the data, you’ll create a Set of exercises for each day. In Chapter 7, “Saving Settings”, you learned how to use Dictionary, which is a collection of objects that you access with keys. A Set is an unordered collection of unique objects. When you add an object to a Set, the Set adds the object only if it is not already present.

For example, a Set created from this Array of exercises for July 16th:

[Squat, Burpee, Squat, Sun Salute, Sun Salute]

Would contain (in no particular order):

[Squat, Sun Salute, Burpee]

Accumulating the Exercises

➤ In the Model group, open HistoryStore.swift and add a new property to ExerciseDay:

var uniqueExercises: [String] {
  Array(Set(exercises)).sorted(by: <)
}

Here you take the array of exercises, create a set of unique instances, and then return an array created from that set, sorted alphabetically.

➤ In the Views ▸ History Views group, open HistoryView.swift and, in exerciseView, change day.exercises to:

day.uniqueExercises

Instead of listing all the exercises, you list only the unique instances.

➤ Build and run the app, and tap the History button.

You no longer get an error printed in the console as all the values listed are unique.

Unique values
Unique values

Unfortunately the seventeen squats you might have achieved in a day have all been reduced to one listing. Time to add the accumulated number.

➤ Open HistoryStore.swift and add a new method to ExerciseDay:

func countExercise(exercise: String) -> Int {
  exercises.filter { $0 == exercise }.count
}

Here you pass in an exercise name and retrieve all the instances of that exercise from the exercises array. You return the count of those instances.

➤ Back in HistoryView.swift, in exerciseView, add a modifier to Text(exercise):

.badge(day.countExercise(exercise: exercise))

A badge provides supplementary information in a list. This badge will show the number of times you performed an exercise.

➤ Preview the view. Instead of repeating the listings, each exercise now shows the number of times you’ve performed it.

Unique values
Unique values

Lists

Skills you’ll learn in this section: Listing data; deleting items from lists; collapsing hierarchical data; the Edit button

A List is a container that shows elements from a collection of data. Each element is presented on a row. This is similar to the Form you’ve been using, but your current usage is much more a listing of data than creating a form where you might ask for input from the user.

Editable Lists

Being able to edit lists of data is a common requirement. In this app, you may want to reset your daily exercise. Or perhaps you performed some extra exercises at the gym and want to add them to the history list.

The Apple standard way of editing rows in lists is to have both an Edit button in the navigation bar and a swipe action to delete. You’ll first add the swipe action and then add the button. Most of the behavior is built-in.

With your data, each item in the file has a unique date, and the exercises for that date are held in a single array.

Your sample data
Your sample data

This design choice means that your data is separated by date, not by exercise. The top level of your list will be dates, which means that built-in list editing will take place on dates. Of course, with the nested ForEach loop, you can still list the exercises, but editing those exercises is more difficult.

To start with, simplify dayView(day:) so that it shows only dates.

➤ In HistoryView, replace the contents of dayView(day:) with:

Text(day.date.formatted(as: "d MMM YYYY"))
  .font(.headline)

You change the date format and list only dates.

Listing dates
Listing dates

➤ In body, replace Form and its contents with:

List($history.exerciseDays, editActions: [.delete]) { $day in
  dayView(day: day)
}

That’s all that’s required to set up deletion from a list.

When you have a List with a ForEach following, you can compress them into one. The $ binding syntax makes the data mutable so that you can delete it. The edit action here is delete. Another edit action is move, which allows you to move rows around in the list. You can try this out, but it doesn’t make sense to reorder the dates here.

➤ In Live Preview, swipe a date to the left. You’ll first see the Delete button, and you can either continue the swipe or tap the button to delete the row.

The delete button
The delete button

The row disappears from both the list and HistoryStore.exercises. However, you haven’t yet saved the history file on disk. If you run the app in Simulator, the rows will disappear when you delete them, but on the second run, the deleted data reappears.

You’ll save the history data when the user closes the History view.

➤ In body, add this modifier to VStack:

.onDisappear {
  try? history.save()
}

When the view disappears, save the data. However, if the user doesn’t close the History view, but instead leaves the app by swiping up from the bottom, the app may be closed by the system without saving the data. You’ll find out how to overcome this by checking scene phases in Chapter 19, “Saving Files”.

➤ Build and run the app and tap the History button to test that your deletion works. To save the data permanently, make sure you close the History view in the app before exiting the app.

Date deletion
Date deletion

Showing Hierarchical Data

A data hierarchy has a parent and children. In your data, the date is the parent, and the exercises are the children. Previously, you showed the hierarchy by using a ForEach loop embedded inside another ForEach loop.

Another way of showing hierarchical data is to use a disclosure group. This view collapses its contents and you expand them using a disclosure indicator.

➤ Replace dayView(day:) with:

func dayView(day: ExerciseDay) -> some View {
  DisclosureGroup {
    exerciseView(day: day)
  } label: {
    Text(day.date.formatted(as: "d MMM YYYY"))
      .font(.headline)
  }
}

Here you embed your Text view in a DisclosureGroup. The disclosure group, when expanded will reveal exerciseView(day:).

➤ Live Preview the view and tap each date to see your accumulated exercises for that date.

Disclosure groups
Disclosure groups

Note: If you have truly hierarchical data, such as a structure Parent containing a property children: [Parent], where children is an array of the same type as Parent, you can take advantage of SwiftUI’s automatic disclosure by initializing the list with the format List(parents, children: \.children) { parent in ... }. The list will automatically list all parents, with disclosure groups for the children.

Correcting Row Deletion

Something very odd happens when you swipe left on an exercise to delete it. An entire day disappears.

Disappearing date
Disappearing date

On each delete action, your List is deleting the top level of data, which in your case is the date.

➤ In dayView(day:), add this modifier to exerciseView(day:):

.deleteDisabled(true)

Now, you will still be able to delete the date with all the exercises, but you won’t be able to delete a single exercise

Note: You can of course do anything with your data in Swift. If you had the requirement of deleting a single exercise, you might set up your data differently, so that the top level of a list would be by exercise, rather than by date. Alternatively, instead of using the built in editActions of a list, you can use the onDelete(perform:) modifier for deletion and write the deletion code yourself.

The Edit button

In addition to swipe-to-delete, you should implement an Edit button. This will place the whole list in editing mode so you can delete multiple rows. Apple provides a special button which does all the work for you.

➤ In headerView, add this as the first view in the HStack:

EditButton()

This places a standard Edit button in the header view.

Edit button
Edit button

➤ In Live Preview, tap the Edit button to go into edit mode:

Edit mode
Edit mode

When you’ve finished deleting items, tap Done to return to the list.

Adding Data to the List

Skills you’ll learn in this section: Date picker; inverting colors; button feedback

You can now delete out-dated information, but how about adding in those exercises that you perform away from your iPhone? This isn’t as easy as adding list deletion. You’ll create an Add button that loads a calendar view from which you can select a date. You’ll set up a button for each exercise and each time you tap a button, your app will add an exercise to that date.

➤ Add a new property to HistoryView to toggle add mode:

@State private var addMode = false

When addMode is true, you’ll show the calendar view.

➤ In headerView, add a button as the first item in the HStack:

Button {
  addMode = true
} label: {
  Image(systemName: "plus")
}
.padding(.trailing)

Here you create the add button, which sets addMode to true.

Add button
Add button

In the History Views group, create a new SwiftUI View file called AddHistoryView.swift. This is where you’ll create the calendar view.

➤ Add new properties to AddHistoryView:

@Binding var addMode: Bool
@State private var exerciseDate = Date()

➤ In AddHistoryView_Previews, change the contents of previews to:

AddHistoryView(addMode: .constant(true))

You’ll dismiss the calendar view from AddHistoryView, which entails changing addMode. You’ll also need to update the exercise date.

Apple provides a date picker that you can configure in various ways. You can customize the style and control which date components you show.

➤ In AddHistoryView, replace the contents of body with:

VStack {
  DatePicker(
    // 1
    "Choose Date",
    // 2
    selection: $exerciseDate,
    // 3
    in: ...Date(),
    // 4
    displayedComponents: .date)
    // 5
  .datePickerStyle(.graphical)
}
.padding()

Here’s what you can customize on a DatePicker:

  1. The label of the control. This may not display, depending on the style of the date picker.

  2. The binding of the date being selected.

  3. An optional closed range. In this case, you don’t want to let the user select a future date, so you constrain the date range up to today.

  4. Whether you want the date and/or the time to show.

  5. The style of the picker. This can be a wheel, a compact drop-down or, in this case, a full calendar month view.

DatePicker
DatePicker

➤ Add this as the first item at the top of the VStack:

ZStack {
  Text("Add Exercise")
    .font(.title)
  Button("Done") {
    addMode = false
  }
  .frame(maxWidth: .infinity, alignment: .trailing)
}

You add the text heading for the view and the button to dismiss the view. By giving the button an infinitely wide frame with trailing alignment, the text is centered, and the button is in the correct position

➤ Open HistoryView.swift. In body, under List, add this code:

if addMode {
  AddHistoryView(addMode: $addMode)
}

When addMode is true, you show the new calendar view.

➤ Try it out in Live Preview. Tap the + button to see the calendar and tap Done to make the DatePicker disappear.

AddExerciseView
AddExerciseView

To navigate through the calendar, tap the forward and back arrows. To change the month and year via a wheel picker, tap the disclosure indicator next to the month/year. Tap the disclosure indicator again when you’ve selected the month and year.

Change the month and year
Change the month and year

When you’re in add mode, the top navigation buttons are not relevant.

➤ In body, change headerView to:

Group {
  if addMode {
    Text("History")
      .font(.title)
  } else {
    headerView
  }
}

Now when you’re in add mode, the buttons in the navigation bar disappear. You embed the conditional in a group to keep the same padding on both views.

Extra Styling

Add a little pizzazz to the calendar view to make it stand out. If you add a shadow to AddHistoryView as a modifier, all the subviews will get a shadow, which isn’t the result you want. Instead, you’ll add a background color to the view, and add a shadow to that.

➤ In body, add this modifier to AddHistoryView:

.background(Color.primary.colorInvert()
.shadow(color: .primary.opacity(0.5), radius: 7))

Here you change the date picker’s background color to the system’s primary color, and invert it. If the system is in Light Mode, the primary color is black. When you invert black, you get white. This matches the original color of the date picker. You add to the background view a primary colored drop shadow with a 50% opacity.

Adding a shadow to the calendar view
Adding a shadow to the calendar view

Adding the Exercise Buttons

Open AddHistoryView.swift and add a new view to the file:

struct ButtonsView: View {
  @EnvironmentObject var history: HistoryStore
  @Binding var date: Date

  var body: some View {
    HStack {
      ForEach(Exercise.exercises.indices, id: \.self) { index in
        let exerciseName = Exercise.exercises[index].exerciseName
        Button(exerciseName) {
          // save the exercise
        }
      }
    }
    .buttonStyle(EmbossedButtonStyle())
  }
}

Here you create a view that shows a button for each exercise, using the embossed button style from the previous chapter.

➤ In AddHistoryView, add this to the VStack above DatePicker:

ButtonsView(date: $exerciseDate)

You show the buttons and pass the currently selected date to ButtonsView

The exercise buttons
The exercise buttons

When you tap one of these buttons, the interface feels curiously unresponsive.

Feedback When Tapping a Button

➤ Open EmbossedButton.swift and examine EmbossedButtonStyle. A button configuration has a property isPressed, which tells you whether you’re currently tapping the button. You can check this property and style your button accordingly.

You’ll scale the button up temporarily, just while the user is tapping the button.

➤ Add a new property to EmbossedButtonStyle:

var buttonScale = 1.0

➤ At the very end of makeBody(configuration:), add a new modifier to configuration.label:

.scaleEffect(configuration.isPressed ? buttonScale : 1.0)

When the user is pressing the button, the button will scale up to the supplied value. At all other times, the button’s scale won’t change.

➤ Open AddHistoryView.swift and, in ButtonsView.body, change .buttonStyle(EmbossedButtonStyle()) to:

.buttonStyle(EmbossedButtonStyle(buttonScale: 1.5))

The buttons will now scale up when you tap them, giving you feedback on your action.

The button scales on tap
The button scales on tap

Incrementing the Exercise Count

When you tap an exercise, the exercise count for that date should increment.

➤ Open HistoryStore.swift.

addDoneExercise(_:) will add or insert exercises. However, as you can now insert historical dates, you’ll need a new method that inserts the date in the correct position in the array.

➤ Add a new method to HistoryStore:

func addExercise(date: Date, exerciseName: String) {
  let exerciseDay = ExerciseDay(date: date, exercises: [exerciseName])
  // 1
  if let index = exerciseDays.firstIndex(
    where: { $0.date.yearMonthDay <= date.yearMonthDay }) {
    // 2
    if date.isSameDay(as: exerciseDays[index].date) {
      exerciseDays[index].exercises.append(exerciseName)
    // 3
    } else {
      exerciseDays.insert(exerciseDay, at: index)
    }
    // 4
  } else {
    exerciseDays.append(exerciseDay)
  }
  // 5
  try? save()
}

Going through the code:

  1. You find the first index in the exerciseDays array where the date is less than or equal to the passed-in date. The where part is a comparison closure that returns true when the criterion is matched. The index of the first true comparison is then passed back to index. Here, the conditional will fail if the passed-in date is earlier than the array dates. You want to compare the dates on a daily basis, so you use yearMonthDay from DateExtension.swift, to exclude the time.

  2. If you find a date in the array that’s the same as the passed-in date, you append the exercise name to the already existing array element.

  3. If the date doesn’t already exist in the array, then insert it at the appropriate position.

  4. If the date is earlier than all the dates in the array, or the array is empty, then append the date to the array.

  5. Save the history data.

➤ Open AddHistoryView.swift and, in ButtonsView, replace // save the exercise with this:

history.addExercise(date: date, exerciseName: exerciseName)

You call the new method with the currently selected date and the name on the tapped button.

➤ In AddHistoryView_Previews, add this modifier to AddHistoryView(addMode:):

.environmentObject(HistoryStore(preview: true))

ButtonsView accesses HistoryStore through the environment. If you don’t set up the environment somewhere in the hierarchy, the preview will crash.

➤ Open HistoryView.swift and try out adding new exercises in Live Preview. Then, try your app in Simulator to make sure that it all gets saved correctly there too.

Oh my, that's a lot of burpees!
Oh my, that's a lot of burpees!

Charts

Skills you’ll learn in this section: Bar charts; organizing data for charts; line charts; stacked charts

Using your history data, Swift Charts give you an opportunity to graphically summarize how you’re performing. You can show which exercise you perform the most, how often you exercise or how many exercises you perform per day or any selected time period. Discover trends so you can analyze why you sometimes have periods where you’re less enthusiastic about exercising. With just a few lines of code, you can draw beautiful charts.

A chart consists of marks which represent the data. These marks could be points, lines, areas or rectangular bars. The data is categorical, meaning that it can be separated out into categories, or in this case, exercises.

Charts have two axes. One axis plots the individual categories, and the other axis plots the numerical data associated with the categories.

Bar Charts

Bar charts present data using rectangles of different heights.

➤ In the History Views group, create a new SwiftUI View file called BarChartDayView.swift, and add this code to the top of the file:

import Charts

Here you import the Swift Charts framework.

➤ Replace BarChartDayView with:

struct BarChartDayView: View {
  var body: some View {
  // 1
    Chart {
    // 2
      BarMark(
      // 3
        x: .value("Name", "Burpee"),
      // 4
        y: .value("Count", 5))
      // 5
      BarMark(
        x: .value("Name", "Squat"),
        y: .value("Count", 2))
    }
  }
}

Creating a chart needs some explanation:

  1. Declare that you are creating a Swift Chart.
  2. Inside the chart, determine what sort of mark to use. This chart will be a bar chart, but it could also be an area, line or point chart.
  3. On the x-axis, you create a plottable value with a label and a string value.
  4. Similarly, on the y-axis, you create a plottable value with a label and an integer value.
  5. Repeat the marks for each piece of data you want to chart.

Live Preview shows the bar chart with the values that you created.

First bar chart
First bar chart

Now you’ll use the history data to create the chart.

➤ Add a new property to BarChartDayView:

let day: ExerciseDay

The chart will show the exercises performed on a particular day.

➤ Change BarChartDayView_Previews to:

struct BarChartDayView_Previews: PreviewProvider {
  static var history = HistoryStore(preview: true)
  static var previews: some View {
    BarChartDayView(day: history.exerciseDays[0])
      .environmentObject(history)
  }
}

Here you load up the history store with the preview data and pass the first day to the chart.

➤ In BarChartDayView, replace the contents of body with:

Chart {
  ForEach(Exercise.names, id: \.self) { name in
    BarMark(
      x: .value(name, name),
      y: .value("Total Count", day.countExercise(exercise: name)))
    .foregroundStyle(Color("history-bar"))
  }
  RuleMark(y: .value("Exercise", 1))
    .foregroundStyle(.red)
}
.padding()

Exercise.names is defined in Exercise.swift and contains all the names of the exercises. The chart iterates through each exercise and creates a bar mark for each one. The x-axis will display the name of the exercise. For the y-axis, you count the number of times you performed the exercise for the day. You also add a rule mark to show that you should perform at least one of the exercises per day.

foregroundStyle allows you to change the colors of the chart. history-bar is a color defined in Assets.xcassets.

Daily bar chart showing Light and Dark Modes
Daily bar chart showing Light and Dark Modes

Notice that, as the data is dynamic, the chart automatically scales to the largest bar size. The y-axis numerical labels are listed down the right. Each exercise is a group.

This chart is ready for use. You can substitute it for your accumulated exercises in the history list.

➤ Open HistoryView.swift, and locate the definition of dayView(day:). Replace exerciseView(day: day) with:

BarChartDayView(day: day)

➤ In Live Preview, check out your daily chart:

Daily chart
Daily chart

This chart shows you individual exercises by day.

Charting a Week’s Data

Next, you’ll create a bar chart that groups all the exercises by day and shows the latest week’s data.

➤ In the History Views group, create a new SwiftUI View file called BarChartWeekView.swift, and replace the code with:

import SwiftUI
import Charts

struct BarChartWeekView: View {
  @EnvironmentObject var history: HistoryStore

  var body: some View {
    // create bar chart here
    .padding()
  }
}

struct BarChartWeekView_Previews: PreviewProvider {
  static var previews: some View {
    BarChartWeekView()
      .environmentObject(HistoryStore(preview: true))
  }
}

➤ In body, replace // create bar chart here with:

Chart(history.exerciseDays.prefix(7)) { day in
  BarMark(
    x: .value("Date", day.date.dayName),
    y: .value("Total Count", day.exercises.count))
}

Going through this chart:

  1. When you don’t need a separate ForEach, you can initialize Chart with the chart data. You use the first seven elements of exerciseDays. Seven is the maximum value, so if there aren’t seven elements in the array, only the available elements are used.
  2. For each of the days, you combine all the exercises into one bar.

➤ Look at the result in Live Preview.

A week's worth of exercises
A week's worth of exercises

The preview data only has four days of data, and these show from left to right in reverse date order. It’s usual to show week data with the last date on the trailing edge. The preview data skips a day, but the chart doesn’t show zero exercises on that day. You can ensure that the chart shows all days by choosing a unit.

➤ Change x: .value("Date", day.date.dayName), to:

x: .value("Date", day.date, unit: .day),

By choosing a unit, the missing day now shows up, and the chart presents the data in standard week-date format with the latest date at the trailing edge.

A daily chart
A daily chart

Because the preview data only contains four days’ worth of data, you don’t get the full seven days. At times like this, you’ll have to massage your data into a format that works with your desired chart.

➤ In BarChartWeekView, create a new property to hold one weeks’ data:

@State private var weekData: [ExerciseDay] = []

➤ In body, add a new modifier to Chart:

.onAppear {
  // 1
  let firstDate = history.exerciseDays.first?.date ?? Date()
  // 2
  let dates = firstDate.previousSevenDays
  // 3
  weekData = dates.map { date in
    history.exerciseDays.first(
      where: { $0.date.isSameDay(as: date) })
    ?? ExerciseDay(date: date)
  }
}

Here you create an array of seven dates. By iterating through these dates, you find out whether you performed any exercises on that date. If you have, you use that data, otherwise you create an empty daily record for that date.

Going through the code:

  1. Find out the first date in history. If there isn’t one, use the current date.
  2. Set up an array using a method already created for you in DateExtension.swift.
  3. Iterate through the array of dates and for each date, locate the first entry for that date. If there isn’t one, create a new blank ExerciseDay.

➤ In body, replace Chart(history.exerciseDays.prefix(7)) { day in with:

Chart(weekData) { day in

In the preview, you’ll now see a seven-day chart.

A seven-day chart
A seven-day chart

Line Charts

It’s easy to replace this bar chart with a line chart.

➤ In body, replace BarMark with:

LineMark

In Live Preview, you’ll see the chart change to a line chart.

A basic line chart
A basic line chart

You can make the chart a bit prettier with some modifiers.

➤ Add these modifiers to LineMark:

.symbol(.circle)
.interpolationMethod(.catmullRom)

At each data point, it draws a circle. Check the code completion in Xcode to see what other symbols you can use. A Catmull-Rom spline interpolates the points along a curve, making the chart smooth instead of linear.

A line chart
A line chart

Other Chart Styles

Try replacing LineMark with PointMark, AreaMark and RectangleMark to see the resulting charts. You can even layer marks by placing one mark after another inside Chart { }.

This is an area chart with a point chart layered on top of it. The area chart has a gradient foreground style, and the point chart has a purple foreground style.

An area chart with a point chart
An area chart with a point chart

Stacked Bar Chart

➤ Return Chart and its contents to:

Chart(weekData) { day in
  BarMark(
    x: .value("Date", day.date, unit: .day),
    y: .value("Total Count", day.exercises.count))
}

With this bar chart, your exercises are all counted together. This doesn’t help if you want to compare your burpees to your squats. You can split out your exercises using similar code to your list when you accumulated the exercise.

➤ Replace Chart and its contents to:

Chart(weekData) { day in
  ForEach(Exercise.names, id: \.self) { name in
    BarMark(
      x: .value("Date", day.date, unit: .day),
      y: .value("Total Count", day.countExercise(exercise: name)))
  }
}

For each day, you iterate through all the four exercise names. Exercise.names is a property in Exercise.swift. You accumulate the current exercises into the bar mark. The result of this chart is currently the same as the previous chart, but you’re now able to split the bars into different colors.

➤ Add a new modifier to BarMark:

.foregroundStyle(by: .value("Exercise", name))

Instead of using a color to determine the style, you separate out the bar by exercise.

In Live Preview, the chart displays the bars with colors marking the relative number of exercises. Beneath the chart, the legend explains what each color represents.

A stacked bar chart
A stacked bar chart

Naturally, you can customize the chart with different colors.

➤ Add a new modifier to Chart:

.chartForegroundStyleScale([
  "Burpee": Color("chart-burpee"),
  "Squat": Color("chart-squat"),
  "Step Up": Color("chart-step-up"),
  "Sun Salute": Color("chart-sun-salute")
])

Assets.xcassets contains these colors for your charts.

The preview updates the legend and the chart with your new colors.

Custom colors
Custom colors

Privacy

Skills you’ll learn in this section: User privacy

Collection and analysis of data over time can be very useful. You can track weight trends with health data or wealth with financial data. Like Google and Apple, you can decide how to collect users’ data, what to do with it and how present it back to your users. If your app attracts enough users, you might be able to create machine learning datasets and use those datasets for future apps.

Fortunately, Apple enforces user privacy. Apple’s article Protecting the User’s Privacy tells you how to access and protect user data. Remember that your users trust you!

Challenge

As you can see, it’s easy to design new charts. Your challenge is to incorporate new charts into your app.

In WelcomeView.swift, add a new button beside the History button called Reports. When you tap this button, you should show a modal view with a Toggle to show a bar or a line chart for the last week. Integrate your existing BarChartWeekView in the modal view.

Your challenge
Your challenge

As always, you can examine the project in your challenge folder for this chapter. In addition, the challenge project has a styled modal timer view that you can examine.

A styled modal timer view
A styled modal timer view

Key Points

  • A Set is a collection of data where each element is unique. Both Set and Array have initializers to create one from the other.
  • Use List for lists of data. Editing lists is built-in.
  • To show groups of data which you can collapse and expand, use a DisclosureGroup.
  • Swift Charts is a framework that displays your data in gorgeous charts with minimal code.
  • As well as bar charts, you can just as easily create line, point and area charts.
  • You can layer charts on top of each other, such as layering points on top of lines.
  • When you have groups of data, you can stack the data in a single bar. Charts will automatically create different colors for the groups.
  • You can customize any chart legends and colors.

Where to Go From Here?

For more practice with Swift Charts, visit Swift Charts Tutorial: Getting Started

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.