How to create custom slider by using SwiftUI?

10,634

Solution 1

As it turned out for me accent color is depending on the context, as well as the frame, so we don't need to handle that.

As far as the control goes I made a really dummy and simple example. Please do not consider this as a solution, rather a starter.

struct CustomView: View {

    @Binding var percentage: Float // or some value binded

    var body: some View {
        GeometryReader { geometry in
            // TODO: - there might be a need for horizontal and vertical alignments
            ZStack(alignment: .leading) {
                Rectangle()
                    .foregroundColor(.gray)
                Rectangle()
                    .foregroundColor(.accentColor)
                    .frame(width: geometry.size.width * CGFloat(self.percentage / 100))
            }
            .cornerRadius(12)
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged({ value in
                    // TODO: - maybe use other logic here
                    self.percentage = min(max(0, Float(value.location.x / geometry.size.width * 100)), 100)
                }))
        }
    }
}

You can use it like

    @State var percentage: Float = 50

    ...
    var body: some View {
    ...
            CustomView(percentage: $percentage)
                .accentColor(.red)
                .frame(width: 200, height: 44)
    ...

Solution 2

In my case, I had to customize the thumb. (ex. screen locker)

I leave an answer for a problem similar one.


enter image description here



LockerSlider.swift

import SwiftUI

struct LockerSlider<V>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {

    // MARK: - Value
    // MARK: Private
    @Binding private var value: V
    private let bounds: ClosedRange<V>
    private let step: V.Stride

    private let length: CGFloat    = 50
    private let lineWidth: CGFloat = 2

    @State private var ratio: CGFloat   = 0
    @State private var startX: CGFloat? = nil


    // MARK: - Initializer
    init(value: Binding<V>, in bounds: ClosedRange<V>, step: V.Stride = 1) {
        _value  = value
    
        self.bounds = bounds
        self.step   = step
    }


    // MARK: - View
    // MARK: Public
    var body: some View {
        GeometryReader { proxy in
            ZStack(alignment: .leading) {
                // Track
                RoundedRectangle(cornerRadius: length / 2)
                    .foregroundColor(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)))
            
                // Thumb
                Circle()
                    .foregroundColor(.white)
                    .frame(width: length, height: length)
                    .offset(x: (proxy.size.width - length) * ratio)
                    .gesture(DragGesture(minimumDistance: 0)
                                .onChanged({ updateStatus(value: $0, proxy: proxy) })
                                .onEnded { _ in startX = nil })
            }
            .frame(height: length)
            .overlay(overlay)
            .simultaneousGesture(DragGesture(minimumDistance: 0)
                                    .onChanged({ update(value: $0, proxy: proxy) }))
            .onAppear {
                ratio = min(1, max(0,CGFloat(value / bounds.upperBound)))
            }
        }
    }

    // MARK: Private
    private var overlay: some View {
        RoundedRectangle(cornerRadius: (length + lineWidth) / 2)
            .stroke(Color.gray, lineWidth: lineWidth)
            .frame(height: length + lineWidth)
    }


    // MARK: - Function
    // MARK: Private
    private func updateStatus(value: DragGesture.Value, proxy: GeometryProxy) {
        guard startX == nil else { return }
    
        let delta = value.startLocation.x - (proxy.size.width - length) * ratio
        startX = (length < value.startLocation.x && 0 < delta) ? delta : value.startLocation.x
    }

    private func update(value: DragGesture.Value, proxy: GeometryProxy) {
        guard let x = startX else { return }
        startX = min(length, max(0, x))
    
        var point = value.location.x - x
        let delta = proxy.size.width - length
    
        // Check the boundary
        if point < 0 {
            startX = value.location.x
            point = 0
        
        } else if delta < point {
            startX = value.location.x - delta
            point = delta
        }
    
        // Ratio
        var ratio = point / delta
    
    
        // Step
        if step != 1 {
            let unit = CGFloat(step) / CGFloat(bounds.upperBound)
        
            let remainder = ratio.remainder(dividingBy: unit)
            if remainder != 0 {
                ratio = ratio - CGFloat(remainder)
            }
        }
    
        self.ratio = ratio
        self.value = V(bounds.upperBound) * V(ratio)
    }
}

Demo.swift

import SwiftUI

struct Demo: View {

    // MARK: - Value
    // MARK: Private
    @State private var number = 150000.0


    // MARK - View
    // MARK: Public
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Number\n\(number)")
                .bold()
                .padding(.bottom, 20)
        
            Text("OS Slider")
            Slider(value: $number, in: 0...1050000, step: 0.02)
                .padding(.bottom, 20)
        
            Text("Custom Slider")
            LockerSlider(value: $number, in: 0...1050000, step: 0.02)
                .padding(.bottom, 20)
        }
        .padding(20)
    }
}
Share:
10,634
Mohammad Mugish
Author by

Mohammad Mugish

Updated on June 30, 2022

Comments

  • Mohammad Mugish
    Mohammad Mugish almost 2 years

    I am able to create a Slider by using SwiftUI but I am not able to change the style of the slider as shown in the image(below).

    Problem: I am not able to find any option in SwiftUI to change the slider style.

    Note: I want to create this by using SwiftUI only. I already created this slider in Swift by using "https://github.com/daprice/iOS-Tactile-Slider"

    I have tried following but it's not the solution :

    1. Slider(value: .constant(0.3)).accentColor(Color.white)
    
    2. Slider(value: $age, in: 18...20, step: 1, minimumValueLabel: Text("18"), maximumValueLabel: Text("20")) { Text("") }
    
    3. Slider(value: $age, in: 18...20, step: 1, minimumValueLabel: Image(systemName: "18.circle"), maximumValueLabel: Image(systemName: "20.circle")) { Text("") }
    

    How can I create a slider with the style as shown in the image using SwiftUI only?

    enter image description here

    • gujci
      gujci over 4 years
      Sorry, first, I have not read it correctly. I started to look into "how to pass parameters to UIViewRepresentable". But later realised, you wanted the full swiftUI stuff. So I guess the question in how can you extract the modifier information from the original view.
    • Mohammad Mugish
      Mohammad Mugish over 4 years
      Yes. There are two things. First is, Create Slider from scratch by using SwiftUI or Second is to Create Slider by extracting something from the UIKit library. Both are good options because my focus is to design this slider in SwiftUI.
    • gujci
      gujci over 4 years
      For the second thing, wrapping it is the solution, if I understand you correctly. This snippet might be something for starter. gist.github.com/Gujci/7a7c37ce6a4bc29c498ca3c593bf2b69
    • gujci
      gujci over 4 years
      Also, accentColor depends on the context, so, if you make a new swiftUI view and use .foregroundColor(.accentColor) it will be the one, you set on the outer level.
    • gujci
      gujci over 4 years
      Also I don't think, that SwiftUI color can be translated back to UIKit with a public API.
  • Mohammad Mugish
    Mohammad Mugish over 4 years
    Thanks for this solution. I got it.
  • Mohammad Mugish
    Mohammad Mugish over 4 years
    I found one problem with this. If I want to change the value of the slider by just tapping anywhere on the slider, but the value is not getting changed. I need to drag it, then the only value is getting changed. How can I change the value by just tapping anywhere on the slider?
  • gujci
    gujci over 4 years
    LOL, It's much more simple, use DragGesture(minimumDistance: 0). Found here
  • eResourcesInc
    eResourcesInc over 4 years
    This got me on the right path. Combine this with an @EnvironmentObject holding an audio player, and you have got the start of a simple, dynamic SwiftUI audio player tracker
  • JohnKubik
    JohnKubik over 2 years
    Hey Den! This code is amazing. I was wondering if you had some time where I could pay you to tutor me on swift. I am trying to understand how this works, and getting hung up on how the heck the number gets updated. Thanks!
  • Den
    Den over 2 years
    @JohnKubik I think WWDC is the best tutor. Swift and SwiftUI are still babies, so you can learn quickly and easily through WWDC and keep up with the latest technology. I also recommend the free iOS courses from Stanford University. You can learn about SwiftUI. youtube.com/watch?v=bqu6BquVi2M