Last active
April 1, 2019 13:51
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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) | |
} | |
} | |
} |
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
How to implement it?