A simple progress bar with customizable segments

I’ve seen lots of progress bar widgets floating around so I decided to try my hand at a ‘SegmentedProgressBar’ customizable via Interface Builder. To follow along or quickly add this view to your own project, grab it on GitHub.

Before diving into the code, I zeroed in on two main features that I wanted to include:

1) Customized Segments

Segments should be fully customizable including width, height, background color, borderWidth, borderRadius, borderColor. Similarly, lines separating segments should be customizable as well: Line height, line width (distance between segments), line color.

2) IBDesignables

IBDesignables allow Interface Builder to render UI changes of a custom view in real time. Combined with IBInspectables, users can edit specific runtime variables and see the result instantly saving considerable development time.

Getting Started

First things first, we need to set up a custom class with support for IBDesignables. The @IBDesignable tag lets Interface Builder know that this class should be rendered automatically.

Now that the class has been created we can start to add customization options. Lets start by creating some segment properties which can be edited from IB. Change the class of any UIView to SegmentedProgressBar to gain access to these variables.

Marking variables as IBInspectable gives developers the ability to view and edit these properties from within Interface Builder. Here I have added properties for the number of total segments, segment width, segment height, and segment color. Note: I am using a didSet{} for numberOfSegments because at least 2 segments are required to form a line. While users will be able to set a value less than 2 via Interface Builder, only values >= 2 will be recognized.

As you can see, all properties defined as IBInspectable variables are visible in the attribute inspector. Now to build our first segment! I am going to override the view’s onDraw() function in order to render all UI changes made to the class in real time. All custom draw logic for every segment and line will take place within this function.

Here I created a new UIView named ‘segment1’ centered inside the parent with a width, height, and color taken from our inspectable variables. Editing any of these variables should change the shape and color of ‘segment1’ in real time. Note: If Interface Builder does not appear to be rendering any changes, you may have to enable ‘Automatically Refresh Views’ in the editor menu.

Building the Progress Bar

Now we have our first segment, but two more elements are required: a second segment and the connecting line between them.

First, I created new IBInspectable height, width, and color variables for the line. Next I created the line view itself centered in the parent with it’s length and width taken from these new line variables. You can also now see that the X position of segment1 is centered to the leftmost edge of the line. Finally, I created segment2 using the IBInspectable variables and centered it to the line’s right most edge.

At this point, the draw() function is getting huge. Lets refactor draw() into drawLine() and drawSegments() , then make some instance variables to keep track of our views. We’re going to create two lists of UIView , one for the segments and one for the lines, so that we can interact with each element later on.

Draw Segments Dynamically

Now, we have a basic progress bar with two segments and a connecting line. Unfortunately, the segments and line are created manually and do not correspond with the numberOfSegments variable. Lets fix that. When building this progress bar there are two fundamental states to consider: an odd number of segments, or an even number of segments.

If odd, one segment will be centered in the parent with lines on the left and right leading to additional segments.If even, a line will be centered in the parent with segments on either end.

Since we already have an even number of segments as our base case lets start with evens. First, we need to create a few variables to handle dynamic generation of lines and segments.

leftLineAnchor and rightLineAnchor represent the minimum X coordinate of the leftmost line, and maximum X coordinate for the right most line. They will be used to position our left and right segments. Direction is an enum which dictates the direction to draw our lines.

Now we can refactor our draw(_ rect: CGRect) function to draw either an even or odd number of segments.

Before we touch on the buildEventSegments() and buildOddSegments() functions, lets update drawLine() and drawSegments() . We will use .insert(UIView, 0) for Direction.left and .append(UIView) for Direction.right.

drawLine() has been updated to create a new line according to passed direction. Default case: Build line centered at midX , midY and update left and right anchors. Left Direction: Build line extending left from leftLineAnchor and update leftLineAnchor with new minX. Right direction: Builds line extending right from rightLineAnchor and updates rightLineAnchor with new maxX . As previously stated, the lineAnchor variables are used to position new segments and draw future lines.

Not much has changed here, drawSegments() is now drawSegment(x: CGFloat) , and has been updated to build a new segment centered at a passed in X coordinate.

First, we use drawLine(direction: .start) to create a new line centered in the view. Since we are passing .start as the direction, leftLineAnchor and rightLineAnchor will be updated to the new line’s minX and maxX . Next, we loop through the number of segments: if the index is even, the new segment will be centered around leftLineAnchor . If odd, the new segment will be centered around rightLineAnchor . After we have drawn a new segment on the left and right sides, check to see if there are any remaining segments to be drawn. If (index < numberOfSegments -1) : more segments exist, new lines are redrawn on the left and right sides, and line anchor variables are updated.

Now we are ready to build progress bars with an even number of segments! Change the numberOfSegments variable to any even number and customize the line and segment options however you like. The progress bar should be drawn to the screen as it was previously.

Now to add the buildOddSegments() function. Most of the logic has already been completed in drawLine() and drawSegment() , so this implementation should be fairly similar to buildEvenSegments() .

First draw the center segment with drawSegment(x: bounds.midX) . The left and right lines should start from the center of this first segment, so reset the line anchor variables to midX as well. Next, we simply loop through numberOfSegment s drawing a new line and segment for each segment found. Similar to buildEvenSegments() , an even index indicates a left segment, while odd represents right. Note: we used numberOfSegments-1 because we’ve already placed one segment with the first drawSegment() call.

Great, now we have a working progress bar for any number of segments! Everything looks good, except many of the segments are being drawn behind the lines. Lets add a quick function to update the z axis of the segments after all lines have been built.

Making it Work

Now we have a working progress bar with some basic customization options, but it doesn’t really do anything. Oftentimes developers will create an onboarding process consisting of multiple steps which need to be completed in order. Lets add an indicator so that users will know which step of the process they are on. We want future developers to be able to tap into this feature, so lets give them the ability to increase or decrease the progress programatically.

First, lets add some new IBInspectable variables so that developers can customize the starting selected index and selected color.

I am using another didSet{} here so that the selected index is always valid. Now, lets add a function to change the color of the selected segment.

Make sure to call this method at the bottom of drawProgressBar() to update the current selection: changeSelectedIndex(index: selectedIndex) . We should now see the first segment background has been updated according to the selectedColor variable from Interface Builder. Increasing or decreasing the selectedIndex should increment or decrement the selected segment accordingly.

Now that we can update the selectedIndex manually from interface builder, lets create some functions to give developers the ability to do this programatically.

Final Polish

The progress bar is really starting to come together, but we need to add a few more customization options before its fully complete. Developers should be able to edit the cornerRadius , borderWidth , and borderColor of the segments. Furthermore, we should add a circularSegment option in case developers want to make all segments circular without calculating a specific corner radius.

First lets add some new inspectable variables for cornerRadius , borderWidth , borderColor , and circularSegments .

For segments to be perfectly circular, their width and height need to be equal. Here I am using didSet{} to ensure that segmentWidth == segmentHeight before enabling circularSegments .

Next create a function to customize each drawn segment.