Custom SegementedControl in SwiftUI

Pascal Boelck
Jul 13 · 6 min read

Good day everyone,

The purpose of this note, is to show you my implementation of a SegmentedControl in SwiftUI. Now, many of you may be thinking, SwiftUI has already implemented a SegmentedControl, why would I need a custom version of it? You’re absolutely right in thinking this. Apple has already implemented a version of SegmentedControl. It is a PickerView that is assigned a style — SegmentedPickerStyle. It looks like the SegmentedControl on the screenshot below.

One of the problems with this current feature is that it is not entirely customizable. If you would like to change the font colour of the text or the background of selected of unselected items, this is unfortunately not possible — only the text itself can be customized.

It is a pity that you can’t fully customize it visually to align with your requirements. For some of it may be enough, but for those who want to customize their SegmentedControl to align with their individual needs, they will most likely find the current limitations frustrating. Unfortunately, this current style cannot be customized or inherited to create a custom SegmentedPickerStyle.

For this reason an, improved SegmentedControl its needed. The final result, will look like this.

Step 1:

Only text and without recognizable selection.

We create a SegmentedControl out of a list of Strings titles: [String]. Following this, we let the index of each string be the output with the help of a Foreach loop ForEach(titles.indices, id: \.self) { index in }). The real item of our segmented control is in this case a button. In the button action closure, the button index is assigned to the global variable selectedIndex. The label closure includes a Text View that displays the string via the index.

Thereafter, we apply the padding modifier to the button to get spacing between the texts and a foregroundColor with black. The view should look similar to the screenshot below.

Step 2:

Recognizable Selection

I would like to add some optical feedback to distinguish the selected item from the other items. This implementation uses a red line below the text and for that, we must know the frame of each button. At first, we need an array that stores frames via an index. That will be initialized in the init function with the count of elements like the count of titles.

Now we come to the tricky part. The logic of SwiftUI is that the parent Views submit properties to their child’s views. Unfortunately, it does not work in an inverted way so easily. We must create our PreferenceKey struct. This PreferenceKey can be used to pass values upwards from the child view to the parent view.

We use the GeometryReader to read the frame of each button. The button gets the ViewModifier background. This includes the GeometryReader to determining the frame of the button, with the help of a colour view and the function call preference — it is possible to store this frame information in the global frame variable. It is important to use the global frame of the geometry parameter, as this gets the coordinates of the button on the whole screen. On the other hand, the local frame gets the coordinates inside the button.

The global frames variables get the property wrapper @State assigned to it, so that it can store the values. Now it is possible to highlight the selected item. At the first, we wrap the Foreach loop with another view — like a VStack, HStack, or a ZStack. This step is important and a reason why the View Modifier does not work with a Foreach loop.

We then extend this wrapper view with a background ViewModifier — this includes a red rectangle. We take the width for this rectangle out of the global frame’s variable via the global selected index variable. The height in this case is 2. The correct horizontal position of the rectangle is set by the offset ViewModifier. This value is the minX from the selected index of frames. To create the gray line for all items we need another gray rectangle, whose height you set to 1. After setting, we use the alignment parameter of the background ViewModifier to position the rectangle in the left corner of the button.

To animate the changes in the positions of the red line, you must add the Animation ViewModifier after the background.

Congratulations, you have implemented your segmented control. This should resemble the following animation.

Step 3:

Scrollable Segmented Control

If you want to provide the SegmentedControl with more values than the current screen width allows, you must make it scrollable.

A scrollable SegmentedControl has the negative feature of the values being left aligned. This is not generally wrong, but if you feed the SegmentedControl with too few values, it can negatively affect your design.

I will show you an implementation where the control itself decides whether needs a scrollView or not. Firstly, you put the SegmentedControl into a ScrollView, select horizontal as the axis parameter and disable the indicator.

This ScrollView will be surrounded by one of the stack views. Here you are free to decide which stack to choose. You just need a container view that you can use to get the width again. You will need to add a background ViewModifier to the container view, as was done for the HStack in the SegmentedControl (for the individual buttons). This gives you the width of the entire SegmentedControl. The background modifier uses a global variable backgroundFrame to store the Width value. This variable uses the PropertyWrapper @State.

Now we know the width of the SegmentedControl because it adapts to the available space. The next step is to compare the sum of the width of all the buttons with the width of the SegmentedControl. There is a global variable @State var isScrollable: Bool to store the result of the calculation.

This function is called in both background ViewModifiers.

Finally, the ScrollView is surrounded by an „if-condition“ in which the variable is checked.

Congratulations the customizable scrollable SegmentedControl is ready.

In the next step you could adjust the font and text colour, or you use a click behaviour with a buttonStyle. Additionally, you can modify the underline to a background shape with rounded edges like this in the image below.

It is also possible to use a picture instead of the text, or both if you wish.

There are a lot of possibilities to advance this control — it is totally up to you. With these improvements your possibilities have increased.

Conclusion

Thank you for reading my article.

I hope you found this article helpful. If you have any queries or suggestions, feel free to comment below and I’ll answer them as soon as I can. Thanks.

You can find the full code on Github.

Axel Springer Tech

Tech and product folks writing about their work across the…