Creating an iOS bar chart in code using Swift

In this post, I’ll build a simple bar chart in code, and add it to a the default UIViewController created by an Xcode template project.

The techniques you’ll learn in this article are:

  • Creating a UIView entirely in code
  • Adding Auto Layout constraints to views in the hierarchy using code
  • Calculating view sizes and relationships at runtime
  • Applying a corner radius to a UIView
  • Using a tap gesture recognizer to detect user interaction
  • Using hit testing to determine which subview (i.e. bar) is underneath a user tap

Requirements

Here are the requirements for the simple bar chart. Read the requirements and think about why this is not a good candidate a storyboard view, and why creating this view in code is a better choice.

  • The chart will contain 1 or more vertical columns (bars), spaced equally in the view
  • The height of each bar will reflect the bar’s relative value compared to all other bars
  • When the user taps on a bar, it will be highlighted, and all non-selected bars will not be highlighted.

The following shows what the final bar chart product will look like, and how the view hierarchy will be constructed to support requirements.

Why does a coded UIView make sense here?

Which requirement(s) aren’t a good fit for a Storyboard (or XIB)? Probably all of them — but especially the first one. The number of views (bars) in the chart won’t be known until runtime. Storyboards aren’t really designed for dynamic numbers of views, so designing this view in code will actually be easier than using a Storyboard.

It may be possible to meet these requirements-at least in part-using a horizontal UICollectionView configured in a Storyboard, and controlled in the UIViewController via delegates. However, a coded UIView is still better since a standard view hierarchy will provide better control over the appearance and interactivity of the chart.

Implementation of the UIView

Let’s start with the chart UIView first. After we code it, we’ll create a Storyboard-based UIViewController and add this UIView to the storyboard. This type of hybrid approach — custom views added to storyboards- is common, and reinforces that storyboard vs. code doesn’t have to be either/or…they can be used together.

In practice you’d probably create the Storyboard, and add the UIView to it. This way you can run the app in the simulator during view development to see how it’s evolving as you go. But for the purposes of making this post flow linearly, I’ll talk through the UIView first.

Create TutorialChartView.swift

  1. In Xcode, select File/New, or press ⌘N
  2. Select the Cocoa Touch source file type
  3. Name the new class TutorialChartView

Create a View Model for the TutorialChartView

The chart has certain data elements that drive its appearance, such as the data values, number of bars, etc. We’ll put this data into a View Model to maintain separation of concerns between model and view.

  1. In Xcode, select File/New, or press ⌘N
  2. Select the Swift File source file type
  3. Name the new class TutorialChartViewModel

Create the View Model

The View Model is simple for this View. It mainly collects the data that will be displayed, the bar color (which is a constant since this is a tutorial), and a few calculated properties to help the view know how to set constraint values.

Create the Main Structure of the TutorialChartView

First, add the following structure to the TutorialChartView.swift file. This will establish the basic structure of the custom UIView, which will be discussed below.

This establishes the top-level structure of the custom view. First, let’s overview what these methods will do. Then we’ll implement and explain them one at a time.

  • viewModel is used to store the data that drives the appearance of the view.
  • tapRecognizer will be used to detect a tap in the view.
  • setData is called from the View Controller when the chart should display a new data set.
  • clearViews is a utility function that removes all current content from the chart view.
  • createChart loops through the data points, creating a UIView for each bar, and applying styling and Auto Layout constraints to size and position each bar
  • createBarView is called by createChart to create an individual bar. Called once for each data point in the dataPoints array
  • createGapView creates an invisible view to maintain the gap between bars.
  • handleBarTap is called when the user taps on the chart. It uses hit testing to find the bar that the user is tapping on.

setData

This method is called from the View Controller (typically) to provide bar data to the chart. It does the following:

  • Saves the data in the View Model
  • Clears the view hierarchy
  • Checks that the data isn’t empty
  • Calls createChart() to create the bars and gaps (which will automatically cause the chart to redraw)

clearViews

This small routine simply removes all subviews from the chart view. In the demo app UI, the chart is set with new data when the user selects the number of bars from a segment control. This routine ensures unused views will be removed and released from memory as the new chart state is drawn.

createChart

This routine does the work of adding UIViews to the chart to represent bars and the gaps between bars.

Important notes for this code:

  • The Bar and Gap UIViews are created by methods createBarView and createGapView (respectively). We’ll cover those methods below.
  • A gap bar isn’t added before the first bar, since the gap is only needed between the bars.
  • Bar widths, bar gaps, and the bar heights are all calculated relative to the width and height of the containing view. By calculating sizes in this way, the chart will size itself appropriately on any screen size and device (i.e. iPad landscape, iPhone portrait, etc.).
  • By creating constraints using the .constraint(…) methods, it’s simple to wrap if/then statements around them so the logic for where constraints are placed is clean and concise.

Note that when creating constraints, they are disabled by default. If the “isActive=true” is not added to a constraint call, the constraint will be added, but will not be used at runtime. In older versions of Xcode this was easy to overlook (I did all the time!). Mercifully, Xcode 12 will warn you if you forget to specify the isActive state when creating a constraint. 👍🏻

createBarView

This routine creates a single bar as a UIView.

The pattern for adding a subview to a UIView is consistent and worth memorizing:

  1. Create the UIView
  2. Add it to the view hierarchy by calling addSubView(..)
  3. Turn off auto-creation of constraints by setting translatesAutoresizingMaskIntoConstraints to false
  4. Add constraints
  5. Configure other view properties as needed

Note: adding constraints before adding the view to its parent view will cause a crash. Get in the habit of always calling addSubview after creating a view!

Note: if you forget to set translatesAutoresizingMaskIntoConstraints to false, strange layouts will result. Essentially iOS will fully constrain the view according to its assumptions, and the constraints you add will be conflicting constraints. iOS will respond by disabling some constraints, but probably not the ones you would choose!

createGapView

Similar to createBarView, this routine adds invisible views between bars. The gaps are calculated as a proportion of the overall width (i.e. gaps should be 20% of the view width). Using these invisible views allows us to use the .constraint(width, multiplier) constraint to calculate the gap width.

If the bar gap was fixed, then we could instead set the left edge of each bar to the right edge of the previous bar, plus a constant value. This would eliminate the need for the invisible gap views. I like the proportional sizing, though, so I elected this approach.

One additional note: even though the gap views are invisible, iOS considers it an error if we don’t provide information to set the vertical position and height for these invisible views. While the view would look ok without the heightAnchor and centerYAnchor constraints, iOS would generate purple layout warnings when running the app. Be mindful of layout warnings and strive to eliminate them.

At this point, the layout is complete, and if we used the chart view, it would render bars according to the data provided in the setData(..) method. Hooray!

Implement the UITapGestureRecognizer

The final feature for the chart is the tap gesture recognizer, so let’s finish by implementing that feature.

We already added the tapRecognizer instance variable to keep a reference to the recognizer, but we didn’t actually initialize it yet.

Below an init method is added, and within init(coder) the tapRecognizer is initialized and the tapRecognizer method is set as the handler for the tap.

The init?(coder:) method added is the init method called by Interface Builder when the UIView is embedded in a storyboard. For this tutorial, this is the only initializer needed. However, if the TutorialChartView will be created from code, it would need an appropriate initializer (i.e. default or accepting CGRect), because init(coder:) would not be called in that case.

Add Hit Testing to find the bar being tapped

At this point the handleBarTap method will be called whenever the chart is tapped. However, it’s not enough to know that the chart was tapped — we need to know which bar is tapped.

Luckily, iOS provides a hit test function to provide a reference to which subview is tapped when a view tap recognizer is activated. We can implement the hit test in the handleBarTap method to discover which specific subview has been tapped, and then use the bar tag (added in createBarView) to highlight only the bar that was tapped (and not highlight bars that weren’t tapped).

Putting the TutorialChartView together

Here’s the completed TutorialChartView.swift module:

Click to view on GitHub

Adding TutorialChartView to a Storyboard UIViewController

Finally, let’s put the view to work by adding it to a Storyboard view. I won’t go through all the steps to create a basic app with a single main window (I assume you know how to do that). Below is the ViewController I’ve created, which has a TutorialChartView (the green view), and a segment control below it.

In my test implementation, when the user taps on a segment in the segment control, the View Controller creates an array of random values with the selected number of elements, and calls the chart’s setData([Double]) method to redraw the chart.

View Controller code

In the code below, when a segment is selected, an array is created, filled with random values, and sent to the chart for display.

Final Functionality

Here’s an animation of how the final test app looks when running:

GitHub Source Code

The full source code for the project, including the TutorialChartView, its View Model and the ViewController is available in my GitHub repo here.

Originally published at https://www.robkerr.com on February 3, 2021.

Software Engineer (consultant) specializing in #iOS. Blog at http://www.robkerr.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store