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 reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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

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

.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

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

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()
AddHistoryView(addMode: .constant(true))
VStack {
  DatePicker(
    // 1
    "Choose Date",
    // 2
    selection: $exerciseDate,
    // 3
    in: ...Date(),
    // 4
    displayedComponents: .date)
    // 5
  .datePickerStyle(.graphical)
}
.padding()
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

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

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.

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

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

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

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now