Chapters

Hide chapters

macOS Apprentice

First Edition · macOS 13 · Swift 5.7 · Xcode 14.2

Section II: Building With SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

Section III: Building With AppKit

Section 3: 6 chapters
Show chapters Hide chapters

9. Charting Your Progress
Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you created a new window scene to display game statistics. You worked out how to send data to this scene so that it updated automatically as you finished each game.

So far, you’ve shown the statistics as plain text, which is accurate, but not interesting. It’s not even easy to comprehend at a glance.

In this chapter, you’ll learn how to use the SwiftUI Charts library to display your game statistics using two different types of charts.

Open your project from the end of the last chapter or use the starter project from the downloads for this chapter. Run the app, play a few games and then press Command-T to open the Statistics window and see the current display:

Statistics as text.
Statistics as text.

This window could certainly do with an upgrade. :]

Preparing the Data

To draw a chart, you start with data points. Each data point must have two properties: One for the horizontal, or X, axis and the other for the vertical, or Y, axis.

Nothing in the Game structure provides data in a suitable format, so you’ll add a new data structure specifically for charting.

Select Models ▸ Letter.swift in the Project navigator to ensure that your new file is in the right group. Now, right-click the selected file and choose New File… — yet another way to add a file. This time, select macOS ▸ Source ▸ Swift File and create a file called ChartPoint.swift.

Add this to your new file:

// 1
struct ChartPoint: Identifiable {
  // 2
  let id = UUID()
  // 3
  let name: String
  let value: Int
}

What does this structure do?

  1. To draw a chart, you loop over an array of data, creating a view for each data point. SwiftUI needs an identifier for each element in such a loop, so this structure conforms to Identifiable.
  2. Identifiable requires a property called id. Here, you set this property to a UUID. The ChartPoint initializer creates one for every new instance.
  3. The remaining two properties define what appears in the chart. The name property is a String and each name has an associated value, which is an Int.

In previous Identifiable structures, you’ve used integer ids. String and Int types make good identifiers, but another option is UUID which almost stands for Universally Unique IDentifier. :] This is a “128-bit value guaranteed to be unique over both space and time”. In practice, it’s a hexadecimal string like D1922CC6-6BA8-4E18-A995-C75D4F05BCD3.

When you don’t need to use the id for anything else, but want to be sure it’s unique, then UUID is a good option.

Now that you’ve set up ChartPoint, you can start to use it.

Creating Chart Points

Open GameStats.swift and look at where you defined gameReport. This uses the data in games and produces a computed String for display. Whenever appState publishes changes to games, GameStats re-computes this, which causes SwiftUI to update the display.

// 1
var gameStatsPoints: [ChartPoint] {
  // 2
  let wonGames = games.filter {
    $0.gameStatus == .won
  }
  let lostGames = games.filter {
    $0.gameStatus == .lost
  }

  // 3
  let chartPoints = [
    ChartPoint(name: "Wins", value: wonGames.count),
    ChartPoint(name: "Losses", value: lostGames.count)
  ]

  // 4
  return chartPoints
}

Displaying a Chart

Still in GameStats.swift, start at the top by adding this import:

import Charts
// 1
Chart(gameStatsPoints) { point in
  // 2
  BarMark(
    // 3
    x: .value("Count", point.value),
    // 4
    y: .value("Name", point.name)
  )
  // 5
  // bar modifiers here
}
// chart modifiers here
A basic bar chart
A xumex lut nmocb

Providing Preview Content

It gets a bit tedious having to run the app and play a few games to see any statistics data. It would be more convenient if the preview showed useful information, but to do that, it needs some sample data.

// 1
extension Game {
  // 2
  static var sampleGames: [Game] {
    // 3
    var game1 = Game(id: 1)
    game1.word = "SNOWMAN"
    game1.gameStatus = .lost

    // 4
    var game2 = Game(id: 2)
    game2.word = "FROST"
    game2.gameStatus = .won

    var game3 = Game(id: 3)
    game3.word = "ANTARCTICA"
    game3.gameStatus = .won

    // 5
    return [game1, game2, game3]
  }
}
GameStats(games: Game.sampleGames)
Previewing the bar chart.
Cxepoovokt nwe bir xkepb.

Styling the Bar Chart

The bars in the chart are blue by default. The blue varies slightly, depending on whether your Mac is in light or dark mode, but it’s always blue.

.foregroundStyle(.red)

Varying the Colors

There are a couple of ways you can do this. For the first, replace the current foregroundStyle modifier with:

.foregroundStyle(by: .value("Name", point.name))
Colored bars and legend
Tozubij zawl umn qilumt

// 1
.chartForegroundStyleScale([
  // 2
  "Wins": Color.green.gradient,
  "Losses": Color.orange.gradient
])
Defining bar colors.
Yuhajacn viy judowr.

Adding More Style

There are a few more modifiers that’ll make your chart stand out. These all go after the chartForegroundStyleScale modifier.

.frame(minWidth: 250, minHeight: 300)
.padding()
.shadow(radius: 5, x: 5, y: 5)
Bars with shadows and padding.
Bomz vuhk sxanonp osk gixvifw.

Annotating the Bars

Your chart looks impressive, but there’s more you can do. Annotations are a way to add more textual information to the chart.

// 1
.annotation(
  // 2
  position: .overlay,
  // 3
  alignment: .leading,
  // 4
  spacing: 20) {
    // 5
    Text("\(point.name): \(point.value)")
      .font(.title2)
  }
.chartLegend(.hidden)
Final bar chart design
Rapay teq gziqz navuff

Preparing Line Chart Data

You know how to create a bar chart, but SwiftUI can create many different types of charts. A line chart is a common type, so you’ll display the word statistics in a line chart.

// 1
var wordStatsPoints: [ChartPoint] {
  // 2
  let completedGames = games.filter { game in
    game.gameStatus != .inProgress
  }

  // 3
  let chartPoints = completedGames.map { game in
    // 4
    ChartPoint(
      name: "#\(game.id)",
      value: game.word.count)
  }

  // 5
  return chartPoints
}
import Charts
WordStats(games: Game.sampleGames)

Drawing a Line Chart

In body, replace Text(wordCountReport) with:

// 1
Chart {
  // 2
  ForEach(wordStatsPoints) { point in
    // 3
    LineMark(
      // 4
      x: .value("Game ID", point.name),
      // 5
      y: .value("Word Count", point.value)
    )
    // line modifiers here
  }
  
  // another mark here
}
// chart modifiers here
Basic line chart
Tenol tala vwomb

Modifying the Line Chart

The chart goes right to the edge of the view and crops some numbers, so to start, add these after // chart modifiers here:

.frame(minWidth: 250, minHeight: 300)
.padding()
.chartYScale(domain: .automatic(includesZero: false))
Line chart with padding and axis setting.
Heso khivb yits rolpasl aqr ihit retcogk.

Configuring the Lines

By default, the line has a thickness of 2. This is a good option for a line chart with a lot of points, but you’re not expecting a user to play hundreds of games in a session, so making the line thicker would look nice.

.lineStyle(StrokeStyle(lineWidth: 4))
.symbol(.diamond)
.symbolSize(200)
Styling the lines.
Wjjzipw nhe vedac.

Adding Some Color

It’s not possible to vary the symbols or colors of individual points — you get separated lines. But you can use a conditional to adjust the color of the entire chart.

// 1
var lineChartColor: Color {
  // 2
  let wonGamesCount = games.filter {
    $0.gameStatus == .won
  }.count
  // 3
  let lostGamesCount = games.filter {
    $0.gameStatus == .lost
  }.count

  // 4
  if wonGamesCount > lostGamesCount {
    return .green
  } else if wonGamesCount < lostGamesCount {
    return .orange
  }
  
  // 5
  return .blue
}
.foregroundStyle(lineChartColor)
Line colors
Yemo mohubb

Showing More Data

So far, the charts have displayed a single data set, but you can add more than one. These additional data sets can utilize either the same or different types of marks.

RuleMark(y: .value("Average", 7.5))
Chart with a RuleMark.
Rkucn jewq u XareLuzs.

Accessibility

The SwiftUI Charts team made the Charts library accessible by default. To hear this in action, open System Settings. Go to Accessibility ▸ Spoken Content and turn on Speak item under the pointer:

Turn on Spoken Content
Gifd ey Zfebod Quznosr

Spoken Content settings
Wyozix Xaqxozg fagbunzk

Default spoken content
Cixeekw jyegek pafneyq

// 1
.accessibilityLabel("Game \(point.name)")
// 2
.accessibilityValue("had \(point.value) letters in the word")

Challenges

Challenge 1: Flip the bars

The GameStats bar chart has horizontal bars. Can you change it so the bars are vertical?

Challenge 2: Use different marks

You’ve used BarMark, LineMark and RuleMark but there are others. Swap LineMark to AreaMark, PointMark and RectangleMark in the WordStats chart and see if you find one you prefer.

Key Points

  • The SwiftUI Charts library allows you to display data graphically.
  • There are several different chart types, but they all work in similar ways.
  • Once you’ve drawn the chart, you can style the chart or the data points.
  • Accessibility is built-in, but you can customize it.

Where to Go From Here

There are some great videos from WWDC 2022 introducing the Charts library and discussing how to use it effectively:

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