iosswiftuikit

How to make an iOS volume slider-like design element?


This is what I need to create:

This is what i need to create

I need this element's value to be able to change with the user's swipes.

I thought it was UISlider but it’s impossible to configure it the way I need:

 lazy var timeSlider : UISlider = {
        let slider = UISlider()
        slider.minimumValue = 1
        slider.maximumValue = 10
        slider.value = 1
        slider.layer.cornerRadius = 10
        slider.tintColor = UIColor(hexColor: "#F9F5EF")
        slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
        addSectionMarkers(for: slider)
        return slider
    }()

Solution

  • I think it would be easier to make your own using a UIStackView and UIPanGestureRecognizer instead of trying to inherit a UISlider.

    Here's what I came up with:

    Result

    CustomSliderView.swift:

    import UIKit
    
    final class CustomSliderView: UIView {
        
        // MARK: - Properties
        
        var progress: Int = 0 {
            didSet {
                progress = max(0, min(progress, sectionCount))
                updateProgress()
            }
        }
        
        private let sectionCount = 10
        private var sectionHeight: CGFloat = 0
        
        private var startProgress: Int = 0
        private var startTouchY: CGFloat = 0
        
        // MARK: - Views
        
        private let stackView = UIStackView()
        private let filledView = UIView()
        
        // MARK: - Init
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            
            commonInit()
        }
        
        private func commonInit() {
            setupView()
            setupStackView()
            setupFilledView()
            addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
        }
        
        // MARK: - Layout
        
        override func layoutSubviews() {
            super.layoutSubviews()
            sectionHeight = bounds.height / CGFloat(sectionCount)
            updateProgress()
        }
        
        private func setupView() {
            backgroundColor = .clear
            layer.cornerRadius = 10
            layer.masksToBounds = true
        }
        
        private func setupStackView() {
            stackView.axis = .vertical
            stackView.distribution = .fillEqually
            stackView.spacing = 1
            stackView.layer.cornerRadius = 10
            stackView.clipsToBounds = true
            
            addSubview(stackView)
            
            stackView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
                stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
                stackView.topAnchor.constraint(equalTo: topAnchor),
                stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
            ])
            
            for _ in 0..<sectionCount {
                let view = UIView()
                view.backgroundColor = UIColor(white: 0.95, alpha: 1)
                stackView.addArrangedSubview(view)
            }
        }
        
        private func setupFilledView() {
            filledView.backgroundColor = UIColor.brown.withAlphaComponent(0.6)
            addSubview(filledView)
        }
        
        // MARK: - Actions
        
        private func updateProgress() {
            let sectionHeight = bounds.height / CGFloat(sectionCount)
            let height = CGFloat(progress) * sectionHeight
            filledView.frame = CGRect(
                x: 0,
                y: bounds.height - height,
                width: bounds.width,
                height: height
            )
        }
        
        @objc
        private func handlePan(_ gesture: UIPanGestureRecognizer) {
            let location = gesture.location(in: self)
            
            switch gesture.state {
            case .began:
                startTouchY = location.y
                startProgress = progress
                
            case .changed:
                let delta = startTouchY - location.y
                let newProgress = startProgress + Int(delta / sectionHeight)
                progress = newProgress
            default:
                break
            }
        }
    }
    

    Usage:

    view.addSubview(customSlider)
    customSlider.translatesAutoresizingMaskIntoConstraints = false
            
    NSLayoutConstraint.activate([
        customSlider.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        customSlider.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        customSlider.widthAnchor.constraint(equalToConstant: 60),
        customSlider.heightAnchor.constraint(equalToConstant: 200)
    ])