Andrea Bozza
Andrea Bozza

Reputation: 1384

UISlider in swift like youtube app

Can I create a custom UISlider that has buffering progress like the one in the Youtube app? I know that this question may be i possible duplicate; but no one solve correctly the problem in iOS or in Swift

Thank you

Upvotes: 2

Views: 1803

Answers (2)

Andrea Bozza
Andrea Bozza

Reputation: 1384

It works! I added a protocol to handle scrolling of components to have a behaviour similar to UISlider

public protocol SliderDelegate {
    func SliderBeginDragging(slider: bufferdSlider)
    func SliderEndDragging(slider: bufferdSlider)
    func SliderScrub(slider: bufferdSlider)
}

public enum ScrollState: Int, Printable {
case BeginDragging = 0
case EndDragging
case scrub
    public var description: String {
        get {
            switch self {
            case BeginDragging:
                return "BeginDragging"
            case EndDragging:
                return "EndDragging"
            case scrub:
                return "Scrub"
            }
        }
    }
}

public class  bufferdSlider: UIControl
{
    public var sliderDelegate: SliderDelegate!
    var currentPosition : Float = 0.0
    {
        didSet
        {
            updateLayers()
        }
    }
    var currentBuffer : Float = 0.0
    {
        didSet
        {
            updateLayers()
        }
    }
    var backgroundLayerColor : UIColor = UIColor.lightGrayColor()
    var progressLayerColor : UIColor = UIColor.redColor()
    var bufferLayerColor : UIColor = UIColor.darkGrayColor()
    var positionRingLayerColor : UIColor = UIColor.redColor()
    private var backgroundLayer : CAShapeLayer!
    private var progressLayer : CAShapeLayer!
    private var bufferLayer : CAShapeLayer!
    private var positionRingLayer : CAShapeLayer!


    override init(frame: CGRect)
    {
        super.init(frame: frame)
        initialize()
    }

    required public init(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        initialize()
    }

    override public func drawRect(rect: CGRect)
    {
        updateLayers()
    }

    private func initialize()
    {
        self.backgroundColor = UIColor.clearColor()

        backgroundLayer = CAShapeLayer()
        backgroundLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        backgroundLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: self.frame.size.width, height: self.frame.size.height / 2.0)).CGPath
        backgroundLayer.fillColor = backgroundLayerColor.CGColor
        backgroundLayer.backgroundColor = UIColor.clearColor().CGColor
        progressLayer = CAShapeLayer()
        progressLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        bufferLayer = CAShapeLayer()
        bufferLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        positionRingLayer = CAShapeLayer()
        positionRingLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        self.layer.addSublayer(backgroundLayer)
        self.layer.addSublayer(bufferLayer)
        self.layer.addSublayer(progressLayer)
        self.layer.addSublayer(positionRingLayer)
        updateLayers()
    }

    private func updateLayers()
    {
        updateProgressLine()
        updateBufferLine()
        updatePositionRing()
    }

    private func updateProgressLine()
    {
        var w = (self.frame.size.width * CGFloat(currentPosition)) + self.frame.size.height / 4

        if w > self.frame.size.width
        {
            w = self.frame.size.width
        }

        progressLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: w, height: self.frame.size.height / 2)).CGPath
        progressLayer.fillColor = progressLayerColor.CGColor
        progressLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    private func updateBufferLine()
    {
        var w = self.frame.size.width * CGFloat(currentBuffer)

        bufferLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: w, height: self.frame.size.height / 2)).CGPath
        bufferLayer.fillColor = bufferLayerColor.CGColor
        bufferLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    private func updatePositionRing()
    {
        var _x = self.frame.size.width * CGFloat(currentPosition)

        if _x > self.frame.size.width - self.frame.size.height
        {
            _x = self.frame.size.width - self.frame.size.height
        }

        positionRingLayer.path = UIBezierPath(ovalInRect: CGRect(x: _x, y: 0, width: self.frame.size.height, height: self.frame.size.height)).CGPath
        positionRingLayer.fillColor = positionRingLayerColor.CGColor
        positionRingLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    override public func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
        super.beginTrackingWithTouch(touch, withEvent: event)
        println("beginTrackingWithTouch")
        sliderDelegate.SliderBeginDragging(self)
        return true
    }


    override public func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
    {
        super.continueTrackingWithTouch(touch, withEvent: event)

        sliderDelegate.SliderScrub(self)
        let point = touch.locationInView(self)

        var _xb = (self.frame.size.width * CGFloat(currentBuffer)) - self.frame.size.height
        if  (point.x > 0) // if (point.x < _xb) && (point.x > 0) used for seek and play
        {
            currentPosition = Float(point.x / self.frame.size.width)
            self.setNeedsDisplay()
        }
        return true
    }
    override public func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent){
        super.endTrackingWithTouch(touch, withEvent: event)
        println("endTrackingWithTouch")
        sliderDelegate.SliderEndDragging(self)
    }

    override public func cancelTrackingWithEvent(event: UIEvent?) {
        super.cancelTrackingWithEvent(event)
        println("cancelTrackingWithEvent")
        sliderDelegate.SliderEndDragging(self)
    }
}

Upvotes: 2

ifau
ifau

Reputation: 2120

You need subclass UIControl and draw three layers in drawRect: – one for background line, second for buffer line, third for progress, depending on current progress and available buffer values. Example:

class MySlider: UIControl
{
    var currentPosition : Float = 0.0
    {
        didSet
        {
            updateLayers()
        }
    }

    var currentBuffer : Float = 0.0
    {
        didSet
        {
            updateLayers()
        }
    }

    var backgroundLayerColor : UIColor = UIColor.lightGrayColor()
    var progressLayerColor : UIColor = UIColor.redColor()
    var bufferLayerColor : UIColor = UIColor.darkGrayColor()
    var positionRingLayerColor : UIColor = UIColor.redColor()

    private var backgroundLayer : CAShapeLayer!
    private var progressLayer : CAShapeLayer!
    private var bufferLayer : CAShapeLayer!
    private var positionRingLayer : CAShapeLayer!

    override init(frame: CGRect)
    {
        super.init(frame: frame)
        initialize()
    }

    required init(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        initialize()
    }

    override func drawRect(rect: CGRect)
    {
        updateLayers()
    }

    private func initialize()
    {
        self.backgroundColor = UIColor.clearColor()

        backgroundLayer = CAShapeLayer()
        backgroundLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        backgroundLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: self.frame.size.width, height: self.frame.size.height / 2.0)).CGPath
        backgroundLayer.fillColor = backgroundLayerColor.CGColor
        backgroundLayer.backgroundColor = UIColor.clearColor().CGColor

        progressLayer = CAShapeLayer()
        progressLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)

        bufferLayer = CAShapeLayer()
        bufferLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)

        positionRingLayer = CAShapeLayer()
        positionRingLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)

        self.layer.addSublayer(backgroundLayer)
        self.layer.addSublayer(bufferLayer)
        self.layer.addSublayer(progressLayer)
        self.layer.addSublayer(positionRingLayer)

        updateLayers()
    }

    private func updateLayers()
    {
        updateProgressLine()
        updateBufferLine()
        updatePositionRing()
    }

    private func updateProgressLine()
    {
        var w = (self.frame.size.width * CGFloat(currentPosition)) + self.frame.size.height / 4

        if w > self.frame.size.width
        {
            w = self.frame.size.width
        }

        progressLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: w, height: self.frame.size.height / 2)).CGPath
        progressLayer.fillColor = progressLayerColor.CGColor
        progressLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    private func updateBufferLine()
    {
        var w = self.frame.size.width * CGFloat(currentBuffer)

        bufferLayer.path = UIBezierPath(rect: CGRect(x: 0, y: (self.frame.size.height / 2) - self.frame.size.height / 4, width: w, height: self.frame.size.height / 2)).CGPath
        bufferLayer.fillColor = bufferLayerColor.CGColor
        bufferLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    private func updatePositionRing()
    {
        var _x = self.frame.size.width * CGFloat(currentPosition)

        if _x > self.frame.size.width - self.frame.size.height
        {
            _x = self.frame.size.width - self.frame.size.height
        }

        positionRingLayer.path = UIBezierPath(ovalInRect: CGRect(x: _x, y: 0, width: self.frame.size.height, height: self.frame.size.height)).CGPath
        positionRingLayer.fillColor = positionRingLayerColor.CGColor
        positionRingLayer.backgroundColor = UIColor.clearColor().CGColor
    }

    override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
    {
        super.continueTrackingWithTouch(touch, withEvent: event)
        let point = touch.locationInView(self)

        var _xb = (self.frame.size.width * CGFloat(currentBuffer)) - self.frame.size.height
        if (point.x < _xb) && (point.x > 0)
        {
            currentPosition = Float(point.x / self.frame.size.width)
            self.setNeedsDisplay()
        }
        return true
    }
}

In some view controller:

let slider = MySlider(frame: CGRect(x: 50, y: 100, width: 200, height: 30))
self.view.addSubview(slider)
slider.currentPosition = 0.3
slider.currentBuffer = 0.7

Result:

custom slider

Upvotes: 9

Related Questions