How to customise the Slider in SwiftUI

How to customise the Slider in SwiftUI

There is limited ability to customise the Slider in SwiftUI as of Xcode 13. These are three options of presenting a custom view of the Slider in a SwiftUI App. Set the tint color and background color of the slider; fall back to using the UISliderView from UIKit or create a custom SliderView from scratch in SwiftUI.

These are 3 options to customising a Slider control in SwiftUI:

  1. Set tint and background color
  2. Use UISlider from UIKit wrapped in a UIViewRepresentable
  3. Create a custom slider from scratch in SwiftUI


Set tint and background color

There are great advantages to using the default SwiftUI controls like Slider. They can be added to a view with just a couple of lines of code, they adapt well to different layouts and sizes and they tend to have accessibility baked in. Sometimes, the default appearance is not the desired look required in an app and we look for ways to customise the appearance. Unfortunately, there are limited options available to customise the look and feel of the Slider.

 1struct ContentView: View {
 2    @State private var currentValue = 6.0
 3    
 4    var body: some View {
 5        VStack(spacing:30) {
 6            Text("SwiftUI Slider")
 7                .font(.title2)
 8                .fontWeight(.bold)
 9            
10            Text("value = \(currentValue, specifier: "%.2F")")
11            
12            VStack(spacing:0) {
13                Text("Default Slider")
14                Slider(
15                    value: $currentValue,
16                    in: 0...10.0
17                )
18                    .frame(width: 200)
19            }
20            
21            VStack(spacing:0) {
22                Text("background color")
23                Slider(
24                    value: $currentValue,
25                    in: 0...10.0
26                )
27                    .background(Color.yellow)
28                    .frame(width: 200)
29            }
30            
31            VStack(spacing:0) {
32                Text("tint color")
33                Slider(
34                    value: $currentValue,
35                    in: 0...10.0
36                )
37                    .tint(Color.yellow)
38                    .frame(width: 200)
39            }
40            
41            VStack(spacing:0) {
42                Text("accent color")
43                Slider(
44                    value: $currentValue,
45                    in: 0...10.0
46                )
47                    .accentColor(Color.yellow)
48                    .frame(width: 200)
49            }
50            
51            Spacer()
52        }
53    }
54}

Set tint and background color on SwiftUI Slider



Use UISlider from UIKit

The second option to customise more of the features of the Slider is to use the UISlider from UIKit. This needs to be wrapped in UIViewRepresentable and different properties of the UISlider can be exposed to the SwiftUI View. Note that the system does not automatically communicate changes occurring within the UISliderView to other parts of your SwiftUI interface. A Coordinator instance is required to facilitate coordination with other SwiftUI views and user interaction.

 1struct UISliderView: UIViewRepresentable {
 2    @Binding var value: Double
 3    
 4    var minValue = 1.0
 5    var maxValue = 100.0
 6    var thumbColor: UIColor = .white
 7    var minTrackColor: UIColor = .blue
 8    var maxTrackColor: UIColor = .lightGray
 9    
10    class Coordinator: NSObject {
11        var value: Binding<Double>
12        
13        init(value: Binding<Double>) {
14            self.value = value
15        }
16        
17        @objc func valueChanged(_ sender: UISlider) {
18            self.value.wrappedValue = Double(sender.value)
19        }
20    }
21    
22    func makeCoordinator() -> UISliderView.Coordinator {
23        Coordinator(value: $value)
24    }
25    
26    func makeUIView(context: Context) -> UISlider {
27        let slider = UISlider(frame: .zero)
28        slider.thumbTintColor = thumbColor
29        slider.minimumTrackTintColor = minTrackColor
30        slider.maximumTrackTintColor = maxTrackColor
31        slider.minimumValue = Float(minValue)
32        slider.maximumValue = Float(maxValue)
33        slider.value = Float(value)
34        
35        slider.addTarget(
36            context.coordinator,
37            action: #selector(Coordinator.valueChanged(_:)),
38            for: .valueChanged
39        )
40        
41        return slider
42    }
43    
44    func updateUIView(_ uiView: UISlider, context: Context) {
45        uiView.value = Float(value)
46    }
47}

The UISliderView can be used in a SwiftUI view with the optional parameters to specify colors for the thumb and track colors.

 1struct ContentView: View {
 2    @State private var currentValue = 6.0
 3
 4    var body: some View {
 5        VStack(spacing:30) {
 6            Text("SwiftUI Slider")
 7                .font(.title2)
 8                .fontWeight(.bold)
 9            
10            Text("value = \(currentValue, specifier: "%.2F")")
11            
12            VStack(spacing:0) {
13                Text("UISlider")
14                UISliderView(value: $currentValue,
15                             minValue: 1.0,
16                             maxValue: 10.0)
17                    .frame(width: 200)
18            }
19            
20            VStack(spacing:0) {
21                Text("UISlider with colors")
22                UISliderView(value: $currentValue,
23                             minValue: 1.0,
24                             maxValue: 10.0,
25                             thumbColor: .red,
26                             minTrackColor: .purple,
27                             maxTrackColor: .green)
28                    .frame(width: 200)
29            }
30            
31            VStack(spacing:0) {
32                Text("UISlider with colors")
33                UISliderView(value: $currentValue,
34                             minValue: 1.0,
35                             maxValue: 10.0,
36                             thumbColor: .clear,
37                             minTrackColor: .purple,
38                             maxTrackColor: .clear)
39                    .frame(width: 200)
40            }
41            
42            Spacer()
43        }
44    }
45}

Use UISlider in SwiftUI to customise the colors



Create Custom Slider view

The third option is to define a custom slider-style view from scratch in SwiftUI. This obviously offers the most flexibility regarding look and feel, but requires the most effort and may not be suitable for applications that are expected to work in a number of environments. This starts out easy enough with a rounded rectangle as the track for the slider and a circle for the thumb.

A DragGesture is used on the circle to allow the user to interact with the slider and checks are placed to ensure the circle stays inside the rounded rectangle.

 1struct SliderView1: View {
 2    @Binding var value: Double
 3    
 4    @State var lastCoordinateValue: CGFloat = 0.0
 5    
 6    var body: some View {
 7        GeometryReader { gr in
 8            let thumbSize = gr.size.height * 0.8
 9            let radius = gr.size.height * 0.5
10            let minValue = gr.size.width * 0.015
11            let maxValue = (gr.size.width * 0.98) - thumbSize
12            
13            ZStack {
14                RoundedRectangle(cornerRadius: radius)
15                    .foregroundColor(.gray)
16                HStack {
17                    Circle()
18                        .foregroundColor(Color.white)
19                        .frame(width: thumbSize, height: thumbSize)
20                        .offset(x: self.value)
21                        .gesture(
22                            DragGesture(minimumDistance: 0)
23                                .onChanged { v in
24                                    if (abs(v.translation.width) < 0.1) {
25                                        self.lastCoordinateValue = self.value
26                                    }
27                                    if v.translation.width > 0 {
28                                        self.value = min(maxValue, self.lastCoordinateValue + v.translation.width)
29                                    } else {
30                                        self.value = max(minValue, self.lastCoordinateValue + v.translation.width)
31                                    }
32                                    
33                                }
34                        )
35                    Spacer()
36                }
37            }
38        }
39    }
40}

The SliderView1 can be used specifying different height and widths to create different sliders.

 1struct ContentView: View {
 2    @State private var currentValue = 6.0
 3    
 4    var body: some View {
 5        VStack(spacing:20) {
 6            Text("Custom Slider")
 7                .font(.title2)
 8                .fontWeight(.bold)
 9            
10            Text("value = \(currentValue, specifier: "%.2F")")
11            
12            SliderView1(value: $currentValue)
13                .frame(width:300, height:30)
14            
15            SliderView1(value: $currentValue)
16                .frame(width:300, height:10)
17            
18            SliderView1(value: $currentValue)
19                .frame(width:300, height:80)
20            
21            Spacer()
22        }
23    }
24}

Initial custom slider using rounded rectangle and a circle in a ZStack



Custom slider to reflect the value

The problem with the initial slider is that the value of the thumb is the value of the x coordinate and so varies with the width of the Slider.

 1struct SliderView2: View {
 2    @Binding var value: Double
 3    
 4    @State var lastCoordinateValue: CGFloat = 0.0
 5    var sliderRange: ClosedRange<Double> = 1...100
 6    
 7    var body: some View {
 8        GeometryReader { gr in
 9            let thumbSize = gr.size.height * 0.8
10            let radius = gr.size.height * 0.5
11            let minValue = gr.size.width * 0.015
12            let maxValue = (gr.size.width * 0.98) - thumbSize
13            
14            let scaleFactor = (maxValue - minValue) / (sliderRange.upperBound - sliderRange.lowerBound)
15            let lower = sliderRange.lowerBound
16            let sliderVal = (self.value - lower) * scaleFactor + minValue
17            
18            ZStack {
19                RoundedRectangle(cornerRadius: radius)
20                    .foregroundColor(.blue)
21                HStack {
22                    Circle()
23                        .foregroundColor(Color.yellow)
24                        .frame(width: thumbSize, height: thumbSize)
25                        .offset(x: sliderVal)
26                        .gesture(
27                            DragGesture(minimumDistance: 0)
28                                .onChanged { v in
29                                    if (abs(v.translation.width) < 0.1) {
30                                        self.lastCoordinateValue = sliderVal
31                                    }
32                                    if v.translation.width > 0 {
33                                        let nextCoordinateValue = min(maxValue, self.lastCoordinateValue + v.translation.width)
34                                        self.value = ((nextCoordinateValue - minValue) / scaleFactor)  + lower
35                                    } else {
36                                        let nextCoordinateValue = max(minValue, self.lastCoordinateValue + v.translation.width)
37                                        self.value = ((nextCoordinateValue - minValue) / scaleFactor) + lower
38                                    }
39                                    
40                                }
41                        )
42                    Spacer()
43                }
44            }
45        }
46    }
47}

 1struct ContentView: View {
 2    @State private var currentValue1 = 6.0
 3    @State private var currentValue2 = 6.0
 4    
 5    var body: some View {
 6        VStack(spacing:20) {
 7            Text("Custom Slider")
 8                .font(.title2)
 9                .fontWeight(.bold)
10            
11            VStack(spacing:0) {
12                Text("value 1 = \(currentValue1, specifier: "%.2F")")
13                SliderView2(value: $currentValue1)
14                    .frame(width:100, height:30)
15            }
16            
17            VStack(spacing:0) {
18                Text("value 2 = \(currentValue2, specifier: "%.2F")")
19                SliderView2(value: $currentValue2,
20                            sliderRange: 4...10)
21                    .frame(width:200, height:100)
22            }
23            
24            VStack(spacing:0) {
25                Text("value 2 = \(currentValue2, specifier: "%.2F")")
26                SliderView2(value: $currentValue2,
27                            sliderRange: 4...10)
28                    .frame(width:200, height:10)
29            }
30            
31            Spacer()
32        }
33    }
34}

Custom Slider in SwiftUI with value reflecting range for Slider



Set thumb and track colors

The thumb is changed to be a rounded rectangle rather than a circle and the color options are exposed for the track before the thumb and after the thumb as well as the color for the thumb.

 1struct SliderView3: View {
 2    @Binding var value: Double
 3    
 4    @State var lastCoordinateValue: CGFloat = 0.0
 5    var sliderRange: ClosedRange<Double> = 1...100
 6    var thumbColor: Color = .yellow
 7    var minTrackColor: Color = .blue
 8    var maxTrackColor: Color = .gray
 9    
10    var body: some View {
11        GeometryReader { gr in
12            let thumbHeight = gr.size.height * 1.1
13            let thumbWidth = gr.size.width * 0.03
14            let radius = gr.size.height * 0.5
15            let minValue = gr.size.width * 0.015
16            let maxValue = (gr.size.width * 0.98) - thumbWidth
17            
18            let scaleFactor = (maxValue - minValue) / (sliderRange.upperBound - sliderRange.lowerBound)
19            let lower = sliderRange.lowerBound
20            let sliderVal = (self.value - lower) * scaleFactor + minValue
21            
22            ZStack {
23                Rectangle()
24                    .foregroundColor(maxTrackColor)
25                    .frame(width: gr.size.width, height: gr.size.height * 0.95)
26                    .clipShape(RoundedRectangle(cornerRadius: radius))
27                HStack {
28                    Rectangle()
29                        .foregroundColor(minTrackColor)
30                    .frame(width: sliderVal, height: gr.size.height * 0.95)
31                    Spacer()
32                }
33                .clipShape(RoundedRectangle(cornerRadius: radius))
34                HStack {
35                    RoundedRectangle(cornerRadius: radius)
36                        .foregroundColor(thumbColor)
37                        .frame(width: thumbWidth, height: thumbHeight)
38                        .offset(x: sliderVal)
39                        .gesture(
40                            DragGesture(minimumDistance: 0)
41                                .onChanged { v in
42                                    if (abs(v.translation.width) < 0.1) {
43                                        self.lastCoordinateValue = sliderVal
44                                    }
45                                    if v.translation.width > 0 {
46                                        let nextCoordinateValue = min(maxValue, self.lastCoordinateValue + v.translation.width)
47                                        self.value = ((nextCoordinateValue - minValue) / scaleFactor)  + lower
48                                    } else {
49                                        let nextCoordinateValue = max(minValue, self.lastCoordinateValue + v.translation.width)
50                                        self.value = ((nextCoordinateValue - minValue) / scaleFactor) + lower
51                                    }
52                               }
53                        )
54                    Spacer()
55                }
56            }
57        }
58    }
59}

 1struct ContentView: View {
 2    @State private var currentValue1 = 6.0
 3    
 4    var body: some View {
 5        VStack(spacing:20) {
 6            Text("Custom Slider")
 7                .font(.title2)
 8                .fontWeight(.bold)
 9            
10            VStack(spacing:0) {
11                Text("value 1 = \(currentValue1, specifier: "%.2F")")
12                SliderView3(value: $currentValue1,
13                            sliderRange: 4...10)
14                    .frame(width:300, height:30)
15            }
16            
17            VStack(spacing:0) {
18                Text("value 1 = \(currentValue1, specifier: "%.2F")")
19                SliderView3(value: $currentValue1,
20                            sliderRange: 4...10,
21                            thumbColor: .purple,
22                            minTrackColor: .red,
23                            maxTrackColor: .green)
24                    .frame(width:300, height:30)
25            }
26            
27            VStack(spacing:0) {
28                Text("value 1 = \(currentValue1, specifier: "%.2F")")
29                SliderView3(value: $currentValue1,
30                            sliderRange: 4...10,
31                            thumbColor: .red,
32                            minTrackColor: .red,
33                            maxTrackColor: .clear)
34                    .frame(width:300, height:30)
35            }
36            
37            VStack(spacing:0) {
38                Text("value 1 = \(currentValue1, specifier: "%.2F")")
39                SliderView3(value: $currentValue1,
40                            sliderRange: 4...10)
41                    .frame(width:300, height:100)
42            }
43
44            Spacer()
45        }
46    }
47}

Custom Slider in SwiftUI with colors for track and thumb




Conclusion

The is limited options to customise the default Slider in SwiftUI. The tint color or accent color can be used to set the color of the track trailing the thumb. and a background color can be set behind the Slider. There are two other options to further customise the look and feel of a slider control in SwiftUI. One option is to use the UISlider from UIKit and this is probably the most recommended way to go as a lot of the work is done for you in UIKit. The other option is to define your own Slider-style SwiftUI View. This offers the most customisation, but at the cost of having to implement everything. This may seem like the right direction to go, but it could mean implementing some SwiftUI view that is so customised that customers are confused on how to use it. There is a lot more customisation that could be done to the custom SwiftUI Slider and decisions need to be made on which properties need to be exposed. There is great flexibility in creating your own SwiftUI views to create a uniques customer experience.