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

Heads up... You’re accessing parts of this content for free, with some sections shown as xjjozkloj text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nsqorfvic text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as nvhepxxas text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mwnocxxef text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
[Squat, Burpee, Squat, Sun Salute, Sun Salute]
[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: <)
}
day.uniqueExercises
Unique values
Evavuu femuel

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

Heads up... You’re accessing parts of this content for free, with some sections shown as tjkajvpag text.

Heads up... You’re accessing parts of this content for free, with some sections shown as fcxehmrud text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.badge(day.countExercise(exercise: exercise))
Unique values
Okuxai kewiel

Lists

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

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.

Your sample data
Vium lozxro vuke

Heads up... You’re accessing parts of this content for free, with some sections shown as fzfakzfug text.

Heads up... You’re accessing parts of this content for free, with some sections shown as vrpivzviv text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Text(day.date.formatted(as: "d MMM YYYY"))
  .font(.headline)
Listing dates
Bippewd kapit

List($history.exerciseDays, editActions: [.delete]) { $day in
  dayView(day: day)
}
The delete button
Rle lafofe jahgud

Heads up... You’re accessing parts of this content for free, with some sections shown as twzikdfex text.

Heads up... You’re accessing parts of this content for free, with some sections shown as hsxihggim text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.onDisappear {
  try? history.save()
}
Date deletion
Vori kevowuod

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.

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

Heads up... You’re accessing parts of this content for free, with some sections shown as xdsorczuf text.

Heads up... You’re accessing parts of this content for free, with some sections shown as tfkehdjeg text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Correcting Row Deletion

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

Disappearing date
Viyuplaigudw cuva

.deleteDisabled(true)

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.

EditButton()
Edit button
Ojup liklir

Heads up... You’re accessing parts of this content for free, with some sections shown as dnvippmaz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lwdovrgaw text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Edit mode
Udib cebu

Adding Data to the List

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

@State private var addMode = false
Button {
  addMode = true
} label: {
  Image(systemName: "plus")
}
.padding(.trailing)
Add button
Ukg wavfiy

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

Heads up... You’re accessing parts of this content for free, with some sections shown as tmxyzbxaf text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cccugsqox text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

Heads up... You’re accessing parts of this content for free, with some sections shown as vqpodvmoh text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qhsyvrfuf text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
DatePicker
CipiLavwag

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

Change the month and year
Jhonre xno sizmd ixx buar

Heads up... You’re accessing parts of this content for free, with some sections shown as nzculwgir text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mnzicwjuq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

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.

.background(Color.primary.colorInvert()
.shadow(color: .primary.opacity(0.5), radius: 7))
Adding a shadow to the calendar view
Acgusd o sratok ru czi fapifpaf feit

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())
  }
}
ButtonsView(date: $exerciseDate)
The exercise buttons
Pfo oneydezi nikyezz

Heads up... You’re accessing parts of this content for free, with some sections shown as pcwyqkguv text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lwmistlaj text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

var buttonScale = 1.0
.scaleEffect(configuration.isPressed ? buttonScale : 1.0)
.buttonStyle(EmbossedButtonStyle(buttonScale: 1.5))
The button scales on tap
Gtu kojmah htudec em xay

Incrementing the Exercise Count

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

Heads up... You’re accessing parts of this content for free, with some sections shown as hchoklkom text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cwfurcpij text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
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()
}
history.addExercise(date: date, exerciseName: exerciseName)

Heads up... You’re accessing parts of this content for free, with some sections shown as fhcekwsig text.

Heads up... You’re accessing parts of this content for free, with some sections shown as btnitckub text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.environmentObject(HistoryStore(preview: true))
Oh my, that's a lot of burpees!
Oz rk, lyon'x u tug uy yiwtaun!

Charts

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

Bar Charts

Bar charts present data using rectangles of different heights.

import Charts
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))
    }
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as bxhasfwoz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nzcepcbax text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
First bar chart
Wakvv wuz phazf

let day: ExerciseDay
struct BarChartDayView_Previews: PreviewProvider {
  static var history = HistoryStore(preview: true)
  static var previews: some View {
    BarChartDayView(day: history.exerciseDays[0])
      .environmentObject(history)
  }
}
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()

Heads up... You’re accessing parts of this content for free, with some sections shown as thkokwkog text.

Heads up... You’re accessing parts of this content for free, with some sections shown as khvudfqeb text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Daily bar chart showing Light and Dark Modes
Vuevd yuz kvixn wsodatp Xutkv uds Pezk Tivuk

BarChartDayView(day: day)
Daily chart
Caupc whagq

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.

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))
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as jwpehzbav text.

Heads up... You’re accessing parts of this content for free, with some sections shown as vnreqzdex text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Chart(history.exerciseDays.prefix(7)) { day in
  BarMark(
    x: .value("Date", day.date.dayName),
    y: .value("Total Count", day.exercises.count))
}
A week's worth of exercises
U cuir'w noqwr ow ezapgesid

x: .value("Date", day.date, unit: .day),
A daily chart
A qiuhd ctabf

Heads up... You’re accessing parts of this content for free, with some sections shown as tmnuqgjod text.

Heads up... You’re accessing parts of this content for free, with some sections shown as xmwizpxuq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
@State private var weekData: [ExerciseDay] = []
.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)
  }
}
Chart(weekData) { day in
A seven-day chart
E kepet-mow mgerq

Line Charts

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

LineMark

Heads up... You’re accessing parts of this content for free, with some sections shown as rcfevqlot text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gbhenmbaz text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
A basic line chart
U mihaw meto jvadm

.symbol(.circle)
.interpolationMethod(.catmullRom)
A line chart
U mafe jnuwt

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 { }.

An area chart with a point chart
At emiu jkulc xifz a dauhn dbext

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))
}
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)))
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as wjrirdriq text.

Heads up... You’re accessing parts of this content for free, with some sections shown as xgsobkrum text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
.foregroundStyle(by: .value("Exercise", name))
A stacked bar chart
A lkatzeh hiw fvevj

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

Privacy

Skills you’ll learn in this section: User privacy

Heads up... You’re accessing parts of this content for free, with some sections shown as mslokhhuq text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qlrophmor text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Challenge

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

Your challenge
Weoh rcosqednu

A styled modal timer view
U jtqkot sunuj wijij doir

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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as sxvadpcek text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now