Episode #242

Designing a Custom Download Button - Part 1

Series: Large File Downloads

14 minutes
Published on October 27, 2016

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we create a custom control to serve as our download button. We start by creating a circular progress indicator using CAShapeLayer, then move on to subclassing UIControl to provide our image view and touch handling.

Episode Links

Creating a Circular Progress Indicator

class CircularProgressLayer : CAShapeLayer {

    var progress: CGFloat = 0 {
        didSet {
            update()
        }
    }

    var fillLayer: CAShapeLayer!
    var color: UIColor = .lightGray {
        didSet {
            strokeColor = color.cgColor
            fillLayer.fillColor = color.cgColor
        }
    }

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

    init(frame: CGRect) {
        super.init()
        self.frame = frame
        _setupLayer()
    }

    private func _setupLayer() {
        isOpaque = false
        lineWidth = 2
        strokeColor = color.cgColor
        fillColor = UIColor.clear.cgColor

        fillLayer = CAShapeLayer()
        fillLayer.fillColor = color.cgColor
        addSublayer(fillLayer)
        update()
    }

    func update() {
        path = UIBezierPath(ovalIn: bounds).cgPath

        let fillPath = UIBezierPath()
        let radius = frame.size.height/2
        let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
        fillPath.move(to: center)

        let startAngle: CGFloat = -.pi/2
        let endAngle: CGFloat = (2 * .pi ) * progress + startAngle
        fillPath.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        fillLayer.path = fillPath.cgPath
    }
}

Testing out in the storyboard:

let cpl = CircularProgressLayer(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
let page = PlaygroundPage.current

let view = UIView(frame: cpl.bounds.insetBy(dx: -10, dy: -10))
view.backgroundColor = UIColor.white
view.layer.addSublayer(cpl)

func tick() {
    guard cpl.progress <= 1.0 else { return }
    cpl.progress += 0.018
    DispatchQueue.main.async {
        Thread.sleep(forTimeInterval: 0.01)
        tick()
    }
}

page.liveView = view

tick()

Creating a custom Button control

We're not subclassing UIButton because we want to have total control over the UI that is drawn on screen.

class DownloadButton : UIControl {
    lazy var downloadImage = UIImage(named: "Download")
    lazy var completedImage = UIImage(named: "Checkmark")

    var imageView: UIImageView!

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

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

    private func _setupButton() {
        imageView = UIImageView(frame: bounds)
        imageView.contentMode = .scaleAspectFit
        imageView.image = downloadImage
        addSubview(imageView)

        addTarget(self, action: #selector(DownloadButton.addHighlight), for: [.touchDown, .touchDragEnter])
        addTarget(self, action: #selector(DownloadButton.removeHighlight), for: [.touchUpInside, .touchDragExit])
    }

    func addHighlight() {
        backgroundColor = .red
    }

    func removeHighlight() {
        backgroundColor = .clear
    }
}

This episode uses Xcode 8.2, Swift 3.0.