Skip to content

Instantly share code, notes, and snippets.

@Coder-ACJHP
Last active April 1, 2019 13:51
Show Gist options
  • Save Coder-ACJHP/d43f947339d33978f7a596f424ba9864 to your computer and use it in GitHub Desktop.
Save Coder-ACJHP/d43f947339d33978f7a596f424ba9864 to your computer and use it in GitHub Desktop.
File downloader (in background) with animated circular loading progress bar for swift
//
// UICCircularProgressBar.swift
// UICCircularDownloadProgressBar
//
// Created by Coder ACJHP on 1.04.2019.
// Copyright © 2019 Onur Işık. All rights reserved.
//
import UIKit
protocol UICCircularProgressBarDelegate {
func didFinishDownloadAndSave(_ successfully: Bool, filePath: String?)
}
class UICCircularProgressBar: UIView, URLSessionDownloadDelegate {
private let trackLayer = CAShapeLayer()
private let pulsatingLayer = CAShapeLayer()
private var animationLayer = CAShapeLayer()
private let mockUrl = URL(string: "http://techslides.com/demos/sample-videos/small.mp4")
// View options
public private(set) var savedURL: URL?
public var fileName: String = "test"
public var fileExtension: String = "mp4"
public var removeAfterCompletion: Bool = false
public var hideAfterDelay: TimeInterval = 0
public var fontColor = UIColor.black {
didSet {
percentageLabel.textColor = fontColor
completedLabel.textColor = fontColor
}
}
public var lineWidth: CGFloat = 20 {
didSet {
trackLayer.lineWidth = lineWidth
animationLayer.lineWidth = lineWidth
}
}
public var strokeColor = UIColor.red {
didSet {
animationLayer.strokeColor = strokeColor.cgColor
trackLayer.strokeColor = strokeColor.withAlphaComponent(0.6).cgColor
pulsatingLayer.fillColor = strokeColor.withAlphaComponent(0.2).cgColor
}
}
public var fillColor = UIColor.clear {
didSet {
trackLayer.fillColor = fillColor.cgColor
animationLayer.fillColor = fillColor.cgColor
}
}
public var trackStrokeColor = UIColor.lightGray {
didSet {
trackLayer.strokeColor = trackStrokeColor.cgColor
}
}
public var delegate: UICCircularProgressBarDelegate?
private var basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
private lazy var percentageLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: self.frame.width * 0.2)
label.textAlignment = .center
label.textColor = fontColor
label.text = "0"
return label
}()
private lazy var completedLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: self.frame.width * 0.09)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.textColor = fontColor
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
self.adjustSelf()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func adjustSelf() {
self.setupLayers()
self.setupLabels()
self.setupAnimation()
self.initializeSelf()
}
fileprivate func setupLayers() {
let cRadius = self.bounds.width / 2
let cEndAngle = CGFloat.pi * 2
let cStartAngle: CGFloat = 0.0
let cRotateAngle = -CGFloat.pi / 2
let path = UIBezierPath.init(arcCenter: CGPoint.zero, radius: cRadius,
startAngle: cStartAngle, endAngle: cEndAngle, clockwise: true).cgPath
pulsatingLayer.path = path
pulsatingLayer.lineWidth = lineWidth
pulsatingLayer.lineCap = .round
pulsatingLayer.fillColor = strokeColor.withAlphaComponent(0.2).cgColor
layer.addSublayer(pulsatingLayer)
pulsatingLayer.position = self.center
trackLayer.path = path
trackLayer.lineWidth = lineWidth
trackLayer.lineCap = .round
trackLayer.strokeEnd = 1.0
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = strokeColor.withAlphaComponent(0.8).cgColor
layer.addSublayer(trackLayer)
trackLayer.position = self.center
animationLayer.path = path
animationLayer.lineWidth = lineWidth
animationLayer.lineCap = .round
animationLayer.strokeEnd = 0
animationLayer.fillColor = fillColor.cgColor
animationLayer.strokeColor = strokeColor.cgColor
layer.addSublayer(animationLayer)
animationLayer.position = self.center
// We must rotate the layer to start from 12 clock position.
animationLayer.transform = CATransform3DRotate(CATransform3DIdentity, cRotateAngle, 0, 0, 1)
}
fileprivate func setupLabels() {
let stackView = UIStackView(arrangedSubviews: [percentageLabel, completedLabel])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fillProportionally
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(self.frame.width * 0.4)),
stackView.heightAnchor.constraint(equalTo: heightAnchor, constant: -(self.frame.height * 0.4)),
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
fileprivate func setupAnimation() {
// Setup and start pulsating animation
setupPulsatingAnimation()
basicAnimation.fromValue = 0
basicAnimation.repeatCount = 0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
// add animation to animating layer
animationLayer.add(basicAnimation, forKey: "strokeEndAnimation")
}
fileprivate func setupPulsatingAnimation() {
let pulsatingAnimation = CABasicAnimation(keyPath: "transform.scale")
pulsatingAnimation.fromValue = 1.0
pulsatingAnimation.toValue = 1.2
pulsatingAnimation.duration = 0.8
pulsatingAnimation.autoreverses = true
pulsatingAnimation.repeatCount = .infinity
pulsatingAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
pulsatingAnimation.isRemovedOnCompletion = false
// add animation to animating layer
pulsatingLayer.add(pulsatingAnimation, forKey: "grow&Shrink")
}
fileprivate func initializeSelf() {
self.alpha = 0
UIView.animate(withDuration: 1.0, animations: { self.alpha = 1 })
}
fileprivate func hide() {
perform(#selector(hideYourSelf), with: nil, afterDelay: hideAfterDelay)
}
@objc fileprivate func hideYourSelf() {
UIView.animate(withDuration: 1.0, animations: {
self.alpha = 0
}, completion: { (_) in
self.removeFromSuperview()
})
}
public func downloadAndSave(fromUrlString: String?, withName: String, suffix: String) {
fileName = withName
fileExtension = suffix
// Reset stroke end value to get rid of starting from the half of path
animationLayer.strokeEnd = 0
let urlSession = URLSession(configuration: URLSessionConfiguration.default,
delegate: self, delegateQueue: OperationQueue())
var url: URL!
if fromUrlString != nil { url = URL(string: fromUrlString!) } else { url = mockUrl }
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let percentage = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)
DispatchQueue.main.async {
self.percentageLabel.text = "\(Int(percentage * 100))%"
self.animationLayer.strokeEnd = percentage
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
self.completedLabel.text = "COMPLETED!"
if self.removeAfterCompletion {
self.hide()
}
}
guard let httpResponse = downloadTask.response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
debugPrint("server error");
delegate?.didFinishDownloadAndSave(false, filePath: nil)
return
}
do {
let documentsURL = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
savedURL = documentsURL.appendingPathComponent("\(self.fileName).\(self.fileExtension)")
// Check if file exists
if FileManager.default.fileExists(atPath: savedURL!.absoluteString) {
// Delete file
try FileManager.default.removeItem(atPath: savedURL!.absoluteString)
} else {
try FileManager.default.moveItem(at: location, to: savedURL!)
delegate?.didFinishDownloadAndSave(true, filePath: savedURL!.path)
}
} catch {
debugPrint("file error: \(error)")
delegate?.didFinishDownloadAndSave(false, filePath: nil)
}
}
}
@Coder-ACJHP
Copy link
Author

To get rid of APPTransportSecurity error just add these two lines into your plist file:

<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment