Search code examples
iosswiftprogress-bargradientswift5

Custom Circular Slider with gradient colour bar Swift


Here is what I am trying to do:

Screenshot

The screenshot is taken from Iphone:

Screenshot

Code:-

import UIKit
import MSCircularSlider

class HomeViewController: UIViewController {

@IBOutlet weak var lbl_Clock: UILabel!
@IBOutlet weak var circllarSlider: MSCircularSlider!

var timer:Timer?
var timeCount = 0

override func viewDidLoad() {
    super.viewDidLoad()
    UIViewSetting()
}

internal func drawUnfilledCircle(ctx: CGContext, center: CGPoint, radius: CGFloat, lineWidth: CGFloat, maximumAngle: CGFloat, lineCap: CGLineCap) {
    drawArc(ctx: ctx, center: center, radius: radius, lineWidth: 30, fromAngle: 0, toAngle: maximumAngle, lineCap: lineCap)
}

internal func drawArc(ctx: CGContext, center: CGPoint, radius: CGFloat, lineWidth: CGFloat, fromAngle: CGFloat, toAngle: CGFloat, lineCap: CGLineCap) {
    let cartesianFromAngle = toCartesian(toRad(Double(fromAngle)))
    let cartesianToAngle = toCartesian(toRad(Double(toAngle)))
    ctx.addArc(center: center, radius: radius, startAngle: CGFloat(cartesianFromAngle), endAngle: CGFloat(cartesianToAngle), clockwise: false)
    ctx.setLineWidth(lineWidth)
    ctx.setLineCap(lineCap)
    ctx.drawPath(using: CGPathDrawingMode.stroke)
}

private func toCartesian(_ compassRad: Double) -> Double {
    return compassRad - (Double.pi / 2)
}

private func toRad(_ degrees: Double) -> Double {
    return ((Double.pi * degrees) / 180.0)
}

private func toDeg(_ radians: Double) -> Double {
    return ((180.0 * radians) / Double.pi)
}

func UIViewSetting() {
    circllarSlider.unfilledColor = ThemeColor.lightThemeImageTintColor
    circllarSlider.unfilledColor = .lightGray
}

// MARK: - CLOCK_TIMER_METHOD
func timerMethod() {
    timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [self] timer in
        self.timeCount += 1
        print("Timer fired! \(timeCount)")
        let hourValue = self.timeCount/3600
        let hourRemValue = self.timeCount%3600
        let minValue = hourRemValue/60
        let minRemValue = hourRemValue%60
        
        var hourValueStr = ""
        var minValueStr = ""
        
        if hourValue<10{
            hourValueStr = "0\(hourValue.description)"
        } else {
            hourValueStr = hourValue.description
        }
        
        if minValue<10{
            minValueStr = "0\(minValue.description)"
        } else {
            minValueStr = minValue.description
        }
        self.circllarSlider.currentValue = Double(self.timeCount)
    }
}

// MARK: - VIEW_DID_LOAD_CALLING_METHODS
func SetUI(){
    circllarSlider.handleType = .smallCircle
    circllarSlider.handleColor = .systemGreen
    circllarSlider.filledColor = .systemGreen
    circllarSlider.currentValue = Double(timeCount)
   }
}

Question: How to create same Circular Slider gradient colour?

Can someone please explain to me how to do this? I’ve tried with the above code but have not seen any results yet. Currently, I’m using the third-party library MSCircularSlider. Please correct me if I’m doing something wrong, or suggest any third-party library that can be used to create the same type of slider.

Any help would be greatly appreciated


Solution

  • You need to work on learning one task at a time...

    So, let's look at creating the "inner beveled" circle.

    We'll start with creating a view with a CAGradientLayer:

    enter image description here

    Next, we can add another CAGradientLayer, inset a bit from the first one, with the order of the gradient colors reversed:

    enter image description here

    Now, if we add a circular mask to each layer, we can get this:

    enter image description here

    Here's the code for that custom view:

    class RoundBeveledView: UIView {
        
        public var gradColors: [UIColor] = [
            .init(red: 0.95, green: 0.95, blue: 1.0, alpha: 1.0),
            .init(red: 0.80, green: 0.80, blue: 0.90, alpha: 1.0),
        ]
    
        private let outerBevel = CAGradientLayer()
        private let innerBevel = CAGradientLayer()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            
            outerBevel.colors = [
                gradColors[0].cgColor,
                gradColors[1].cgColor,
            ]
            innerBevel.colors = [
                gradColors[1].cgColor,
                gradColors[0].cgColor,
            ]
            
            outerBevel.startPoint = .init(x: 0.20, y: 0.0)
            outerBevel.endPoint = .init(x: 0.80, y: 1.0)
            innerBevel.startPoint = .init(x: 0.20, y: 0.0)
            innerBevel.endPoint = .init(x: 0.80, y: 1.0)
            
            layer.addSublayer(outerBevel)
            layer.addSublayer(innerBevel)
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            outerBevel.frame = bounds
            let obMask = CAShapeLayer()
            obMask.path = CGPath.init(ellipseIn: outerBevel.bounds.insetBy(dx: 8.0, dy: 8.0), transform: nil)
            outerBevel.mask = obMask
            
            innerBevel.frame = outerBevel.frame.insetBy(dx: 12.0, dy: 12.0)
            let ibMask = CAShapeLayer()
            ibMask.path = CGPath.init(ellipseIn: innerBevel.bounds.insetBy(dx: 12.0, dy: 12.0), transform: nil)
            innerBevel.mask = ibMask
        }
        
    }
    

    and a simple controller to show it:

    class ViewController: UIViewController {
    
        let testView = RoundBeveledView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            testView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(testView)
    
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                //testView.widthAnchor.constraint(equalToConstant: 300.0),
                testView.widthAnchor.constraint(equalToConstant: 240.0),
                testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
                testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
        }
        
    }
    

    To complete your goal, you'll also need to add a "base" circle-masked view, a couple layers for shadows, a CAShapeLayer for the "round bar", another layer for the tick-marks, etc.

    See if you can use this example code to get started on the rest. If you need additional help, come back and ask a single, specific question about what problem you're running into.