Search code examples
iosswiftcalayer

Create a layer with increasing height - swift


I want to make a custom slider that has increasing height i.e it's height starts from 4.0 and goes to 6.0.

enter image description here

I have written code for creating a layer but I cannot find a way to increase its height in this manner. Here is my code :

let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
    ctx.addPath(path.cgPath)

    ctx.setFillColor(UIColor.red.cgColor)
    ctx.fillPath()

    let lowerValuePosition = slider.positionForValue(slider.lowerValue)
    let upperValuePosition = slider.positionForValue(slider.upperValue)
    let rect = CGRect(x: 0, y: 0,
                      width: (bounds.width - 36),
                      height: bounds.height)
    ctx.fill(rect)

Solution

  • Because the Minimum and Maximum (left-side & right-side) "Track" images stretch, you may not be able to get what you want with a default UISlider.

    Not too tough to get around it though.

    Basically:

    • create a custom view with your "rounded wedge" shape
    • overlay a UISlider on that custom view
    • "fill" the percentage of the shape when the slider value changes

    Here's the idea, before overlaying them:

    enter image description here

    When we want to overlay the slider on the wedge, set the slider Min/Max track images to clear and it looks like this:

    enter image description here

    We can use a little trick to handle "filling" the shape by percentage:

    • use a gradient background layer
    • mask it with the shape
    • set the gradient colors to red, red, gray, gray
    • set the color locations to [0.0, pct, pct, 1.0]

    That way we get a clean edge, instead of a gradient fade.

    Here's a complete example -- no @IBOutlet or @IBAction connections, so just set a view controller's custom class to WedgeSliderViewController:

    class RoundedWedgeSliderView: UIView {
    
        var leftRadius: CGFloat = 4.0
        var rightRadius: CGFloat = 6.0
    
        // mask shape
        private var cMask = CAShapeLayer()
    
        var pct: Float = 0.0 {
            didSet {
                let p = pct as NSNumber
                // disable layer built-in animation so the update won't "lag"
                CATransaction.begin()
                CATransaction.setDisableActions(true)
                // update gradient locations
                gradientLayer.locations = [
                    0.0, p, p, 1.0
                ]
                CATransaction.commit()
            }
        }
    
        // allows self.layer to be a CAGradientLayer
        override class var layerClass: AnyClass { return CAGradientLayer.self }
        private var gradientLayer: CAGradientLayer {
            return self.layer as! CAGradientLayer
        }
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            // gradient colors will be
            //  red, red, gray, gray
            let colors = [
                UIColor.red.cgColor,
                UIColor.red.cgColor,
                UIColor(white: 0.9, alpha: 1.0).cgColor,
                UIColor(white: 0.9, alpha: 1.0).cgColor,
            ]
            gradientLayer.colors = colors
            // initial gradient color locations
            gradientLayer.locations = [
                0.0, 0.0, 0.0, 1.0
            ]
            // horizontal gradient
            gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
            gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            let r = bounds
            // define the "Rounded Wedge" shape
            let leftCenter = CGPoint(x: r.minX + leftRadius, y: r.midY)
            let rightCenter = CGPoint(x: r.maxX - rightRadius, y: r.midY)
            let bez = UIBezierPath()
            bez.addArc(withCenter: leftCenter, radius: leftRadius, startAngle: .pi * 0.5, endAngle: .pi * 1.5, clockwise: true)
            bez.addArc(withCenter: rightCenter, radius: rightRadius, startAngle: .pi * 1.5, endAngle: .pi * 0.5, clockwise: true)
            bez.close()
            // set the mask layer's path
            cMask.path = bez.cgPath
            // mask self's layer
            layer.mask = cMask
        }
    
    }
    
    class WedgeSliderViewController: UIViewController {
    
        let mySliderView = RoundedWedgeSliderView()
        let theSlider = UISlider()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.addSubview(mySliderView)
            view.addSubview(theSlider)
    
            mySliderView.translatesAutoresizingMaskIntoConstraints = false
            theSlider.translatesAutoresizingMaskIntoConstraints = false
    
            // respect safe area
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
    
                // constrain slider 100-pts from top, 40-pts on each side
                theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
                theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
    
                // constrain mySliderView width to the slider width minus 16-pts
                //  (so we have 8-pt "padding" on each side for the thumb to cover)
                mySliderView.widthAnchor.constraint(equalTo: theSlider.widthAnchor, constant: -16.0),
                // constrain mySliderView to same height as the slider, centered X & Y
                mySliderView.heightAnchor.constraint(equalTo: theSlider.heightAnchor),
                mySliderView.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
                mySliderView.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
    
            ])
    
            // set left- and right-side "track" images to empty images
            theSlider.setMinimumTrackImage(UIImage(), for: .normal)
            theSlider.setMaximumTrackImage(UIImage(), for: .normal)
    
            // add target for the slider
            theSlider.addTarget(self, action: #selector(self.sliderValueChanged(_:)), for: .valueChanged)
    
            // set intitial values
            theSlider.value = 0.0
            mySliderView.pct = 0.0
    
            // end-radii of mySliderView defaults to 4.0 and 6.0
            //  un-comment next line to see the difference
            //mySliderView.rightRadius = 10.0
    
        }
    
        @objc func sliderValueChanged(_ sender: Any) {
            if let s = sender as? UISlider {
                // update mySliderView when the slider changes
                mySliderView.pct = s.value
            }
        }
    
    }