Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Building custom charts in SwiftUI

5 min read 1443

Building Custom Charts In SwiftUI

Charts can help app users better visualize data and more readily identify trends. Additionally, many users find it easier to retain data that is presented graphically.

SwiftUI can be used to build charts for iOS apps faster as it requires less code. SwiftUI offers a rich graphics capability that can be used to create and style a variety of charts across all Apple platforms.

In this article, we’ll use SwiftUI to create bar charts and line charts modeled after those from Apple’s Health app. You’ll be able to use these examples to create and style charts in your own iOS app.

Here are the custom charts we will build:

Bar Chart And Line Chart Examples

Let’s get started!

Creating bar charts in SwiftUI

Bar charts are useful for comparing the values of different groups or subgroups of data. Quarterly revenue by product category or monthly ROI by campaign are common examples of data that would display well as a bar chart.

Building and styling the bars of the bar chart

We’ll start by creating the bars of the chart. We’ll give them a linear gradient coloration. Start by creating a View named BarView:

struct BarView: View {
  var datum: Double
  var colors: [Color]

  var gradient: LinearGradient {
    LinearGradient(gradient: Gradient(colors: colors), startPoint: .top, endPoint: .bottom)
  }

  var body: some View {
    Rectangle()
      .fill(gradient)
      .opacity(datum == 0.0 ? 0.0 : 1.0)
  }
}

First, define two parameters: datum and a Color array. Then, based on the colors parameter, define a computed property gradient.

In the body of the View, declare a Rectangle view and fill it with the gradient. If the datum value is 0.0, then the View is not visible.

We made a custom demo for .
No really. Click here to check it out.

Next, create a file BarChartView.swift and add the following code:

struct BarChartView: View {
  var data: [Double]
  var colors: [Color]

  var highestData: Double {
    let max = data.max() ?? 1.0
    if max == 0 { return 1.0 }
    return max
  }

  var body: some View {
    GeometryReader { geometry in
      HStack(alignment: .bottom, spacing: 4.0) {
        ForEach(data.indices, id: \.self) { index in
          let width = (geometry.size.width / CGFloat(data.count)) - 4.0
          let height = geometry.size.height * data[index] / highestData

          BarView(datum: data[index], colors: colors)
            .frame(width: width, height: height, alignment: .bottom)
        }
      }
    }
  }
}

We first accept an array of data. In this case, we use a Double array, but you could also pass in an object. For the second parameter, we accept a Color array.

Next, we use a computed property highestData to calculate the maximum value data.max(). This value is used to define the maximum height of the bar.

In the body of the View, we start with a GeometryReader and declare a horizontal stack HStack(). We declare a ForEach() and loop over the BarView by providing the individual values.

The geometry parameter is used to determine the width and height of each bar. The width of each bar is calculated by taking the full width and dividing it by the data count: geometry.size.width / CGFloat(data.count). The height of each bar is determined by taking the full height and multiplying it by the ratio of the individual data divided by the maximum height: geometry.size.height * data[index] / highestData.

Adding data to the bar chart

With BarChartView ready, it’s time to use it in a view!

Create a View called ActivityView. For this tutorial, we’ll add mock data with random values:

struct ActivityView: View {
  @State private var moveValues: [Double] = ActivityView.mockData(24, in: 0...300)
  @State private var exerciseValues: [Double] = ActivityView.mockData(24, in: 0...60)
  @State private var standValues: [Double] = ActivityView.mockData(24, in: 0...1)

  var body: some View {
    VStack(alignment: .leading) {
      Text("Move").bold()
        .foregroundColor(.red)

      BarChartView(data: moveValues, colors: [.red, .orange])

      Text("Exercise").bold()
        .foregroundColor(.green)

      BarChartView(data: exerciseValues, colors: [.green, .yellow])

      Text("Stand").bold()
        .foregroundColor(.blue)

      BarChartView(data: standValues, colors: [.blue, .purple])
    }
    .padding()
  }

  static func mockData(_ count: Int, in range: ClosedRange<Double>) -> [Double] {
    (0..<count).map { _ in .random(in: range) }
  }
}

In this example, we first create variables for moveValues, exerciseValues, and standValues.

Next, we create corresponding headings (Move, Exercise, and Stand) for the fitness data, which will be displayed in a vertical stack.

We add BarChartView to pass the relevant data and colors. When the View appears, we provide variables with random values. In your own app, you will call your specific method to load actual data into the charts.

And just like that, we’ve created our first bar chart!

Bar Chart Created in SwiftUI

Creating line charts in SwiftUI

A line chart displays numerical information as a series of data points (or markers) connected by lines. A stock chart, which shows changes in a stock’s price, is a common example of a line chart.

Building and styling the line of the line chart

We’ll start by creating the chart’s lines. Create a View named LineView:

struct LineView: View {
  var dataPoints: [Double]

  var highestPoint: Double {
    let max = dataPoints.max() ?? 1.0
    if max == 0 { return 1.0 }
    return max
  }

  var body: some View {
    GeometryReader { geometry in
      let height = geometry.size.height
      let width = geometry.size.width

      Path { path in
        path.move(to: CGPoint(x: 0, y: height * self.ratio(for: 0)))

        for index in 1..<dataPoints.count {
          path.addLine(to: CGPoint(
            x: CGFloat(index) * width / CGFloat(dataPoints.count - 1),
            y: height * self.ratio(for: index)))
        }
      }
      .stroke(Color.accentColor, style: StrokeStyle(lineWidth: 2, lineJoin: .round))
    }
    .padding(.vertical)
  }

  private func ratio(for index: Int) -> Double {
    dataPoints[index] / highestPoint
  }
}

This View accepts data points in the form of an array — in this case, Double. You could replace this with an object. Next, the View accepts a Color for filling the line stroke.

Similar to the method used in BarChartView, we use a computed property, highestPoint, to calculate the dataPoints.max(). This value is used to define the maximum height of the line.

Inside GeometryReader, we create a Path type that moves to the first point. The location of each point on the line is determined by multiplying the height by the ratio of the individual data point divided by the highest point: height * self.ratio().

We loop over the LineView until reaching the last point, connecting each point with a line.

Building and styling the markers of the line chart

Next, we’ll create the data markers. Create a file LineChartCircleView.swift and add the following code:

struct LineChartCircleView: View {
  var dataPoints: [Double]
  var radius: CGFloat

  var highestPoint: Double {
    let max = dataPoints.max() ?? 1.0
    if max == 0 { return 1.0 }
    return max
  }

  var body: some View {
    GeometryReader { geometry in
      let height = geometry.size.height
      let width = geometry.size.width

      Path { path in
        path.move(to: CGPoint(x: 0, y: (height * self.ratio(for: 0)) - radius))

        path.addArc(center: CGPoint(x: 0, y: height * self.ratio(for: 0)),
                    radius: radius, startAngle: .zero,
                    endAngle: .degrees(360.0), clockwise: false)

        for index in 1..<dataPoints.count {
          path.move(to: CGPoint(
            x: CGFloat(index) * width / CGFloat(dataPoints.count - 1),
            y: height * dataPoints[index] / highestPoint))

          path.addArc(center: CGPoint(
            x: CGFloat(index) * width / CGFloat(dataPoints.count - 1),
            y: height * self.ratio(for: index)),
                      radius: radius, startAngle: .zero,
                      endAngle: .degrees(360.0), clockwise: false)
        }
      }
      .stroke(Color.accentColor, lineWidth: 2)
    }
    .padding(.vertical)
  }

  private func ratio(for index: Int) -> Double {
    dataPoints[index] / highestPoint
  }
}

This View follows similar logic to the LineView. However, instead of creating lines, this View draws a circle for each data point.

We combine the LineView and LineChartCircleView to create the LineChartView:

struct LineChartView: View {
  var dataPoints: [Double]
  var lineColor: Color = .red
  var outerCircleColor: Color = .red
  var innerCircleColor: Color = .white

  var body: some View {
    ZStack {
      LineView(dataPoints: dataPoints)
        .accentColor(lineColor)

      LineChartCircleView(dataPoints: dataPoints, radius: 3.0)
        .accentColor(outerCircleColor)

      LineChartCircleView(dataPoints: dataPoints, radius: 1.0)
        .accentColor(innerCircleColor)
    }
  }
}

Inside a ZStack, we add the LineView with dataPoints and specify an accentColor. On top of this, we add an outerCircleColor with a radius of 3.0. Then we add a top layer innerCircleColor with a radius of 1.0. This results in a white circle with a colored outline.

Adding data to the line chart

Now, we put the LineChartView into a container view, HeartRateView, to pass in the relevant data:

struct HeartRateView: View {
  @State var dataPoints: [Double] = [15, 2, 7, 16, 32, 39, 5, 3, 25, 21]

  var body: some View {
    LineChartView(dataPoints: dataPoints)
      .frame(height: 200)
      .padding(4)
      .background(Color.gray.opacity(0.1).cornerRadius(16))
      .padding()
  }
}

Here’s the line chart that we have created and configured:

Line Chart Create in SwiftUI

Conclusion and further reading

In this article, we demonstrated how easy it is to create and style simple charts in SwiftUI for use across all Apple platforms. Open source libraries can also be helpful for building charts with less effort and reduced development time.

Here are two libraries that are especially useful for creating charts for Apple platforms:

  • SwiftUI ChartView: this library can be used to build a variety of customized charts; it also works well with native SwiftUI elements
  • Charts: this library can be used to create beautiful charts for iOS, tvOS, and macOS

For even more advanced customization and inspiration, check out this list of additional open source libraries.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Leave a Reply