Created
March 9, 2023 02:36
-
-
Save okiookio/a41be4a63a9d50599a3491baa7f89c13 to your computer and use it in GitHub Desktop.
showProgressHUD / dismissProgressHUD
This file contains hidden or 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
import UIKit | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
public extension ProgressHUD { | |
enum AnimationType { | |
case systemActivityIndicator | |
case horizontalCirclesPulse | |
case lineScaling | |
case singleCirclePulse | |
case multipleCirclePulse | |
case singleCircleScaleRipple | |
case multipleCircleScaleRipple | |
case circleSpinFade | |
case lineSpinFade | |
case circleRotateChase | |
case circleStrokeSpin | |
} | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
enum AnimatedIcon { | |
case succeed | |
case failed | |
case added | |
} | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
enum AlertIcon { | |
case heart | |
case doc | |
case bookmark | |
case moon | |
case star | |
case exclamation | |
case flag | |
case message | |
case question | |
case bolt | |
case shuffle | |
case eject | |
case card | |
case rotate | |
case like | |
case dislike | |
case privacy | |
case cart | |
case search | |
var image: UIImage? { | |
switch self { | |
case .heart: return UIImage(systemName: "heart.fill") | |
case .doc: return UIImage(systemName: "doc.fill") | |
case .bookmark: return UIImage(systemName: "bookmark.fill") | |
case .moon: return UIImage(systemName: "moon.fill") | |
case .star: return UIImage(systemName: "star.fill") | |
case .exclamation: return UIImage(systemName: "exclamationmark.triangle.fill") | |
case .flag: return UIImage(systemName: "flag.fill") | |
case .message: return UIImage(systemName: "envelope.fill") | |
case .question: return UIImage(systemName: "questionmark.diamond.fill") | |
case .bolt: return UIImage(systemName: "bolt.fill") | |
case .shuffle: return UIImage(systemName: "shuffle") | |
case .eject: return UIImage(systemName: "eject.fill") | |
case .card: return UIImage(systemName: "creditcard.fill") | |
case .rotate: return UIImage(systemName: "rotate.right.fill") | |
case .like: return UIImage(systemName: "hand.thumbsup.fill") | |
case .dislike: return UIImage(systemName: "hand.thumbsdown.fill") | |
case .privacy: return UIImage(systemName: "hand.raised.fill") | |
case .cart: return UIImage(systemName: "cart.fill") | |
case .search: return UIImage(systemName: "magnifyingglass") | |
} | |
} | |
} | |
} | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
public extension ProgressHUD { | |
class var animationType: AnimationType { | |
get { shared.animationType } | |
set { shared.animationType = newValue } | |
} | |
class var colorBackground: UIColor { | |
get { shared.colorBackground } | |
set { shared.colorBackground = newValue } | |
} | |
class var colorHUD: UIColor { | |
get { shared.colorHUD } | |
set { shared.colorHUD = newValue } | |
} | |
class var colorStatus: UIColor { | |
get { shared.colorStatus } | |
set { shared.colorStatus = newValue } | |
} | |
class var colorAnimation: UIColor { | |
get { shared.colorAnimation } | |
set { shared.colorAnimation = newValue } | |
} | |
class var colorProgress: UIColor { | |
get { shared.colorProgress } | |
set { shared.colorProgress = newValue } | |
} | |
class var fontStatus: UIFont { | |
get { shared.fontStatus } | |
set { shared.fontStatus = newValue } | |
} | |
class var imageSuccess: UIImage { | |
get { shared.imageSuccess } | |
set { shared.imageSuccess = newValue } | |
} | |
class var imageError: UIImage { | |
get { shared.imageError } | |
set { shared.imageError = newValue } | |
} | |
} | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
public extension ProgressHUD { | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func dismiss(completion: ((Bool) -> Void)? = nil) { | |
DispatchQueue.main.async { | |
shared.hudHide(completion: completion) | |
} | |
} | |
func dismiss(completion: ((Bool) -> Void)? = nil) { | |
print(Self.self, #function, tag) | |
DispatchQueue.main.async { | |
self.hudHide(completion: completion) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func show(_ status: String? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, hide: false, interaction: interaction, in: view) | |
} | |
} | |
func show(_ status: String? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
print(Self.self, #function, tag) | |
DispatchQueue.main.async { | |
self.setup(status: status, hide: false, interaction: interaction, in: view) | |
} | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func show(_ status: String? = nil, icon: AlertIcon, interaction: Bool = false, in view: UIView? = nil) { | |
let image = icon.image?.withTintColor(shared.colorAnimation, renderingMode: .alwaysOriginal) | |
DispatchQueue.main.async { | |
shared.setup(status: status, staticImage: image, hide: true, interaction: interaction, in: view) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func show(_ status: String? = nil, icon animatedIcon: AnimatedIcon, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, animatedIcon: animatedIcon, hide: true, interaction: interaction, in: view) | |
} | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showSuccess(_ status: String? = nil, image: UIImage? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, staticImage: image ?? shared.imageSuccess, hide: true, interaction: interaction, in: view) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showError(_ status: String? = nil, image: UIImage? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, staticImage: image ?? shared.imageError, hide: true, interaction: interaction, in: view) | |
} | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showSucceed(_ status: String? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, animatedIcon: .succeed, hide: true, interaction: interaction, in: view) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showFailed(_ status: String? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, animatedIcon: .failed, hide: true, interaction: interaction, in: view) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showAdded(_ status: String? = nil, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, animatedIcon: .added, hide: true, interaction: interaction, in: view) | |
} | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showProgress(_ progress: CGFloat, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(progress: progress, hide: false, interaction: interaction, in: view) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
class func showProgress(_ status: String?, _ progress: CGFloat, interaction: Bool = false, in view: UIView? = nil) { | |
DispatchQueue.main.async { | |
shared.setup(status: status, progress: progress, hide: false, interaction: interaction, in: view) | |
} | |
} | |
} | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
public class ProgressHUD: UIView { | |
private var viewBackground: UIView? | |
private var toolbarHUD: UIToolbar? | |
private var labelStatus: UILabel? | |
private var viewProgress: ProgressView? | |
private var viewAnimation: UIView? | |
private var viewAnimatedIcon: UIView? | |
private var staticImageView: UIImageView? | |
private var timer: Timer? | |
public var animationType = AnimationType.systemActivityIndicator | |
public var colorBackground = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2) | |
public var colorHUD = UIColor.systemGray | |
public var colorStatus = UIColor.label | |
public var colorAnimation = UIColor.lightGray | |
public var colorProgress = UIColor.lightGray | |
public var fontStatus = UIFont.boldSystemFont(ofSize: 24) | |
public var imageSuccess = UIImage.checkmark.withTintColor(UIColor.systemGreen, renderingMode: .alwaysOriginal) | |
public var imageError = UIImage.remove.withTintColor(UIColor.systemRed, renderingMode: .alwaysOriginal) | |
private let keyboardWillShow = UIResponder.keyboardWillShowNotification | |
private let keyboardWillHide = UIResponder.keyboardWillHideNotification | |
private let keyboardDidShow = UIResponder.keyboardDidShowNotification | |
private let keyboardDidHide = UIResponder.keyboardDidHideNotification | |
private let orientationDidChange = UIDevice.orientationDidChangeNotification | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
static let shared: ProgressHUD = { | |
let instance = ProgressHUD() | |
return instance | |
} () | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
convenience init() { | |
self.init(frame: UIScreen.main.bounds) | |
self.alpha = 0 | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
required internal init?(coder: NSCoder) { | |
super.init(coder: coder) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
override private init(frame: CGRect) { | |
super.init(frame: frame) | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setup(status: String? = nil, progress: CGFloat? = nil, animatedIcon: AnimatedIcon? = nil, staticImage: UIImage? = nil, hide: Bool, interaction: Bool, in view: UIView?) { | |
containarView = view | |
setupNotifications() | |
setupBackground(interaction) | |
setupToolbar() | |
setupLabel(status) | |
if (progress == nil) && (animatedIcon == nil) && (staticImage == nil) { setupAnimation() } | |
if (progress != nil) && (animatedIcon == nil) && (staticImage == nil) { setupProgress(progress) } | |
if (progress == nil) && (animatedIcon != nil) && (staticImage == nil) { setupAnimatedIcon(animatedIcon) } | |
if (progress == nil) && (animatedIcon == nil) && (staticImage != nil) { setupStaticImage(staticImage) } | |
setupSize() | |
setupPosition() | |
hudShow() | |
if (hide) { | |
let text = labelStatus?.text ?? "" | |
let delay = Double(text.count) * 0.03 + 1.25 | |
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { _ in | |
self.hudHide() | |
} | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupNotifications() { | |
if (viewBackground == nil) { | |
NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardWillShow, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardWillHide, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardDidShow, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: keyboardDidHide, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(setupPosition(_:)), name: orientationDidChange, object: nil) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private weak var containarView: UIView? | |
private var presentView: UIView { | |
return containarView ?? UIApplication.shared.windows.first ?? UIWindow() | |
} | |
private func setupBackground(_ interaction: Bool) { | |
if (viewBackground == nil) { | |
viewBackground = UIView(frame: presentView.bounds) | |
} | |
presentView.addSubview(viewBackground!) | |
viewBackground?.backgroundColor = interaction ? .clear : colorBackground | |
viewBackground?.isUserInteractionEnabled = (interaction == false) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupToolbar() { | |
if (toolbarHUD == nil) { | |
toolbarHUD = UIToolbar(frame: CGRect.zero) | |
toolbarHUD?.isTranslucent = true | |
toolbarHUD?.clipsToBounds = true | |
toolbarHUD?.layer.cornerRadius = 10 | |
toolbarHUD?.layer.masksToBounds = true | |
viewBackground?.addSubview(toolbarHUD!) | |
} | |
toolbarHUD?.backgroundColor = colorHUD | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupLabel(_ status: String?) { | |
if (labelStatus == nil) { | |
labelStatus = UILabel() | |
labelStatus?.textAlignment = .center | |
labelStatus?.baselineAdjustment = .alignCenters | |
labelStatus?.numberOfLines = 0 | |
toolbarHUD?.addSubview(labelStatus!) | |
} | |
labelStatus?.text = (status != "") ? status : nil | |
labelStatus?.font = fontStatus | |
labelStatus?.textColor = colorStatus | |
labelStatus?.isHidden = (status == nil) ? true : false | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupProgress(_ progress: CGFloat?) { | |
viewAnimation?.removeFromSuperview() | |
viewAnimatedIcon?.removeFromSuperview() | |
staticImageView?.removeFromSuperview() | |
if (viewProgress == nil) { | |
viewProgress = ProgressView(colorProgress) | |
viewProgress?.frame = CGRect(x: 0, y: 0, width: 70, height: 70) | |
} | |
if (viewProgress?.superview == nil) { | |
toolbarHUD?.addSubview(viewProgress!) | |
} | |
viewProgress?.setProgress(progress!) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupAnimation() { | |
viewProgress?.removeFromSuperview() | |
viewAnimatedIcon?.removeFromSuperview() | |
staticImageView?.removeFromSuperview() | |
if (viewAnimation == nil) { | |
viewAnimation = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 60)) | |
} | |
if (viewAnimation?.superview == nil) { | |
toolbarHUD?.addSubview(viewAnimation!) | |
} | |
viewAnimation?.subviews.forEach { | |
$0.removeFromSuperview() | |
} | |
viewAnimation?.layer.sublayers?.forEach { | |
$0.removeFromSuperlayer() | |
} | |
if (animationType == .systemActivityIndicator) { animationSystemActivityIndicator(viewAnimation!) } | |
if (animationType == .horizontalCirclesPulse) { animationHorizontalCirclesPulse(viewAnimation!) } | |
if (animationType == .lineScaling) { animationLineScaling(viewAnimation!) } | |
if (animationType == .singleCirclePulse) { animationSingleCirclePulse(viewAnimation!) } | |
if (animationType == .multipleCirclePulse) { animationMultipleCirclePulse(viewAnimation!) } | |
if (animationType == .singleCircleScaleRipple) { animationSingleCircleScaleRipple(viewAnimation!) } | |
if (animationType == .multipleCircleScaleRipple) { animationMultipleCircleScaleRipple(viewAnimation!) } | |
if (animationType == .circleSpinFade) { animationCircleSpinFade(viewAnimation!) } | |
if (animationType == .lineSpinFade) { animationLineSpinFade(viewAnimation!) } | |
if (animationType == .circleRotateChase) { animationCircleRotateChase(viewAnimation!) } | |
if (animationType == .circleStrokeSpin) { animationCircleStrokeSpin(viewAnimation!) } | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupAnimatedIcon(_ animatedIcon: AnimatedIcon?) { | |
viewProgress?.removeFromSuperview() | |
viewAnimation?.removeFromSuperview() | |
staticImageView?.removeFromSuperview() | |
if (viewAnimatedIcon == nil) { | |
viewAnimatedIcon = UIView(frame: CGRect(x: 0, y: 0, width: 70, height: 70)) | |
} | |
if (viewAnimatedIcon?.superview == nil) { | |
toolbarHUD?.addSubview(viewAnimatedIcon!) | |
} | |
viewAnimatedIcon?.layer.sublayers?.forEach { | |
$0.removeFromSuperlayer() | |
} | |
if (animatedIcon == .succeed) { animatedIconSucceed(viewAnimatedIcon!) } | |
if (animatedIcon == .failed) { animatedIconFailed(viewAnimatedIcon!) } | |
if (animatedIcon == .added) { animatedIconAdded(viewAnimatedIcon!) } | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupStaticImage(_ staticImage: UIImage?) { | |
viewProgress?.removeFromSuperview() | |
viewAnimation?.removeFromSuperview() | |
viewAnimatedIcon?.removeFromSuperview() | |
if (staticImageView == nil) { | |
staticImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 60, height: 60)) | |
} | |
if (staticImageView?.superview == nil) { | |
toolbarHUD?.addSubview(staticImageView!) | |
} | |
staticImageView?.image = staticImage | |
staticImageView?.contentMode = .scaleAspectFit | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func setupSize() { | |
var width: CGFloat = 120 | |
var height: CGFloat = 120 | |
if let text = labelStatus?.text { | |
let sizeMax = CGSize(width: 250, height: 250) | |
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: labelStatus?.font as Any] | |
var rectLabel = text.boundingRect(with: sizeMax, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) | |
width = ceil(rectLabel.size.width) + 60 | |
height = ceil(rectLabel.size.height) + 120 | |
if (width < 120) { width = 120 } | |
rectLabel.origin.x = (width - rectLabel.size.width) / 2 | |
rectLabel.origin.y = (height - rectLabel.size.height) / 2 + 45 | |
labelStatus?.frame = rectLabel | |
} | |
toolbarHUD?.bounds = CGRect(x: 0, y: 0, width: width, height: height) | |
let centerX = width/2 | |
var centerY = height/2 | |
if (labelStatus?.text != nil) { centerY = 55 } | |
viewProgress?.center = CGPoint(x: centerX, y: centerY) | |
viewAnimation?.center = CGPoint(x: centerX, y: centerY) | |
viewAnimatedIcon?.center = CGPoint(x: centerX, y: centerY) | |
staticImageView?.center = CGPoint(x: centerX, y: centerY) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
@objc private func setupPosition(_ notification: Notification? = nil) { | |
var heightKeyboard: CGFloat = 0 | |
var animationDuration: TimeInterval = 0 | |
if let notification = notification { | |
let frameKeyboard = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? CGRect.zero | |
animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0 | |
if (notification.name == keyboardWillShow) || (notification.name == keyboardDidShow) { | |
heightKeyboard = frameKeyboard.size.height | |
} else if (notification.name == keyboardWillHide) || (notification.name == keyboardDidHide) { | |
heightKeyboard = 0 | |
} else { | |
heightKeyboard = keyboardHeight() | |
} | |
} else { | |
heightKeyboard = keyboardHeight() | |
} | |
let screen = presentView.bounds | |
let center = CGPoint(x: screen.size.width/2, y: (screen.size.height-heightKeyboard)/2) | |
UIView.animate(withDuration: animationDuration, delay: 0, options: .allowUserInteraction, animations: { | |
self.toolbarHUD?.center = center | |
self.viewBackground?.frame = screen | |
}, completion: nil) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func keyboardHeight() -> CGFloat { | |
if let keyboardWindowClass = NSClassFromString("UIRemoteKeyboardWindow"), | |
let inputSetContainerView = NSClassFromString("UIInputSetContainerView"), | |
let inputSetHostView = NSClassFromString("UIInputSetHostView") { | |
for window in UIApplication.shared.windows { | |
if window.isKind(of: keyboardWindowClass) { | |
for firstSubView in window.subviews { | |
if firstSubView.isKind(of: inputSetContainerView) { | |
for secondSubView in firstSubView.subviews { | |
if secondSubView.isKind(of: inputSetHostView) { | |
return secondSubView.frame.size.height | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
return 0 | |
} | |
// MARK: - | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func hudShow() { | |
timer?.invalidate() | |
timer = nil | |
if (self.alpha != 1) { | |
self.alpha = 1 | |
toolbarHUD?.alpha = 0 | |
toolbarHUD?.transform = CGAffineTransform(scaleX: 1.4, y: 1.4) | |
UIView.animate(withDuration: 0.15, delay: 0, options: [.allowUserInteraction, .curveEaseIn], animations: { | |
self.toolbarHUD?.transform = CGAffineTransform(scaleX: 1/1.4, y: 1/1.4) | |
self.toolbarHUD?.alpha = 1 | |
}, completion: nil) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func hudHide(completion: ((Bool) -> Void)? = nil) { | |
if (self.alpha == 1) { | |
UIView.animate(withDuration: 0.15, delay: 0, options: [.allowUserInteraction, .curveEaseIn], animations: { | |
self.toolbarHUD?.transform = CGAffineTransform(scaleX: 0.3, y: 0.3) | |
self.toolbarHUD?.alpha = 0 | |
}, completion: { isFinished in | |
self.hudDestroy() | |
self.alpha = 0 | |
completion?(isFinished) | |
}) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func hudDestroy() { | |
NotificationCenter.default.removeObserver(self) | |
staticImageView?.removeFromSuperview(); staticImageView = nil | |
viewAnimatedIcon?.removeFromSuperview(); viewAnimatedIcon = nil | |
viewAnimation?.removeFromSuperview(); viewAnimation = nil | |
viewProgress?.removeFromSuperview(); viewProgress = nil | |
labelStatus?.removeFromSuperview(); labelStatus = nil | |
toolbarHUD?.removeFromSuperview(); toolbarHUD = nil | |
viewBackground?.removeFromSuperview(); viewBackground = nil | |
timer?.invalidate() | |
timer = nil | |
} | |
// MARK: - Animation | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationSystemActivityIndicator(_ view: UIView) { | |
let spinner = UIActivityIndicatorView(style: .large) | |
spinner.frame = view.bounds | |
spinner.color = colorAnimation | |
spinner.hidesWhenStopped = true | |
spinner.startAnimating() | |
spinner.transform = CGAffineTransform(scaleX: 1.6, y: 1.6) | |
view.addSubview(spinner) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationHorizontalCirclesPulse(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let spacing: CGFloat = 3 | |
let radius: CGFloat = (width - spacing * 2) / 3 | |
let ypos: CGFloat = (height - radius) / 2 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes = [0.36, 0.24, 0.12] | |
let timingFunction = CAMediaTimingFunction(controlPoints: 0.2, 0.68, 0.18, 1.08) | |
let animation = CAKeyframeAnimation(keyPath: "transform.scale") | |
animation.keyTimes = [0, 0.5, 1] | |
animation.timingFunctions = [timingFunction, timingFunction] | |
animation.values = [1, 0.3, 1] | |
animation.duration = 1 | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: radius/2, y: radius/2), radius: radius/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
for i in 0..<3 { | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: (radius + spacing) * CGFloat(i), y: ypos, width: radius, height: radius) | |
layer.path = path.cgPath | |
layer.fillColor = colorAnimation.cgColor | |
animation.beginTime = beginTime - beginTimes[i] | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationLineScaling(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let lineWidth = width / 9 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes = [0.5, 0.4, 0.3, 0.2, 0.1] | |
let timingFunction = CAMediaTimingFunction(controlPoints: 0.2, 0.68, 0.18, 1.08) | |
let animation = CAKeyframeAnimation(keyPath: "transform.scale.y") | |
animation.keyTimes = [0, 0.5, 1] | |
animation.timingFunctions = [timingFunction, timingFunction] | |
animation.values = [1, 0.4, 1] | |
animation.duration = 1 | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: height), cornerRadius: width/2) | |
for i in 0..<5 { | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: lineWidth * 2 * CGFloat(i), y: 0, width: lineWidth, height: height) | |
layer.path = path.cgPath | |
layer.backgroundColor = nil | |
layer.fillColor = colorAnimation.cgColor | |
animation.beginTime = beginTime - beginTimes[i] | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationSingleCirclePulse(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let duration: CFTimeInterval = 1.0 | |
let animationScale = CABasicAnimation(keyPath: "transform.scale") | |
animationScale.duration = duration | |
animationScale.fromValue = 0 | |
animationScale.toValue = 1 | |
let animationOpacity = CABasicAnimation(keyPath: "opacity") | |
animationOpacity.duration = duration | |
animationOpacity.fromValue = 1 | |
animationOpacity.toValue = 0 | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationOpacity] | |
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: width/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: width, height: height) | |
layer.path = path.cgPath | |
layer.fillColor = colorAnimation.cgColor | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationMultipleCirclePulse(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let duration = 1.0 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes = [0, 0.3, 0.6] | |
let animationScale = CABasicAnimation(keyPath: "transform.scale") | |
animationScale.duration = duration | |
animationScale.fromValue = 0 | |
animationScale.toValue = 1 | |
let animationOpacity = CAKeyframeAnimation(keyPath: "opacity") | |
animationOpacity.duration = duration | |
animationOpacity.keyTimes = [0, 0.05, 1] | |
animationOpacity.values = [0, 1, 0] | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationOpacity] | |
animation.timingFunction = CAMediaTimingFunction(name: .linear) | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: width/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
for i in 0..<3 { | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: width, height: height) | |
layer.path = path.cgPath | |
layer.fillColor = colorAnimation.cgColor | |
layer.opacity = 0 | |
animation.beginTime = beginTime + beginTimes[i] | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationSingleCircleScaleRipple(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let duration: CFTimeInterval = 1.0 | |
let timingFunction = CAMediaTimingFunction(controlPoints: 0.21, 0.53, 0.56, 0.8) | |
let animationScale = CAKeyframeAnimation(keyPath: "transform.scale") | |
animationScale.keyTimes = [0, 0.7] | |
animationScale.timingFunction = timingFunction | |
animationScale.values = [0.1, 1] | |
animationScale.duration = duration | |
let animationOpacity = CAKeyframeAnimation(keyPath: "opacity") | |
animationOpacity.keyTimes = [0, 0.7, 1] | |
animationOpacity.timingFunctions = [timingFunction, timingFunction] | |
animationOpacity.values = [1, 0.7, 0] | |
animationOpacity.duration = duration | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationOpacity] | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: width/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: width, height: height) | |
layer.path = path.cgPath | |
layer.backgroundColor = nil | |
layer.fillColor = nil | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 3 | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationMultipleCircleScaleRipple(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let duration = 1.25 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes = [0, 0.2, 0.4] | |
let timingFunction = CAMediaTimingFunction(controlPoints: 0.21, 0.53, 0.56, 0.8) | |
let animationScale = CAKeyframeAnimation(keyPath: "transform.scale") | |
animationScale.keyTimes = [0, 0.7] | |
animationScale.timingFunction = timingFunction | |
animationScale.values = [0, 1] | |
animationScale.duration = duration | |
let animationOpacity = CAKeyframeAnimation(keyPath: "opacity") | |
animationOpacity.keyTimes = [0, 0.7, 1] | |
animationOpacity.timingFunctions = [timingFunction, timingFunction] | |
animationOpacity.values = [1, 0.7, 0] | |
animationOpacity.duration = duration | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationOpacity] | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: width/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
for i in 0..<3 { | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: width, height: height) | |
layer.path = path.cgPath | |
layer.backgroundColor = nil | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 3 | |
layer.fillColor = nil | |
animation.beginTime = beginTime + beginTimes[i] | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationCircleSpinFade(_ view: UIView) { | |
let width = view.frame.size.width | |
let spacing: CGFloat = 3 | |
let radius = (width - 4 * spacing) / 3.5 | |
let radiusX = (width - radius) / 2 | |
let duration = 1.0 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes: [CFTimeInterval] = [0.84, 0.72, 0.6, 0.48, 0.36, 0.24, 0.12, 0] | |
let animationScale = CAKeyframeAnimation(keyPath: "transform.scale") | |
animationScale.keyTimes = [0, 0.5, 1] | |
animationScale.values = [1, 0.4, 1] | |
animationScale.duration = duration | |
let animationOpacity = CAKeyframeAnimation(keyPath: "opacity") | |
animationOpacity.keyTimes = [0, 0.5, 1] | |
animationOpacity.values = [1, 0.3, 1] | |
animationOpacity.duration = duration | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationOpacity] | |
animation.timingFunction = CAMediaTimingFunction(name: .linear) | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(arcCenter: CGPoint(x: radius/2, y: radius/2), radius: radius/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
for i in 0..<8 { | |
let angle = .pi / 4 * CGFloat(i) | |
let layer = CAShapeLayer() | |
layer.path = path.cgPath | |
layer.fillColor = colorAnimation.cgColor | |
layer.backgroundColor = nil | |
layer.frame = CGRect(x: radiusX * (cos(angle) + 1), y: radiusX * (sin(angle) + 1), width: radius, height: radius) | |
animation.beginTime = beginTime - beginTimes[i] | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationLineSpinFade(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let spacing: CGFloat = 3 | |
let lineWidth = (width - 4 * spacing) / 5 | |
let lineHeight = (height - 2 * spacing) / 3 | |
let containerSize = max(lineWidth, lineHeight) | |
let radius = width / 2 - containerSize / 2 | |
let duration = 1.2 | |
let beginTime = CACurrentMediaTime() | |
let beginTimes: [CFTimeInterval] = [0.96, 0.84, 0.72, 0.6, 0.48, 0.36, 0.24, 0.12] | |
let timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) | |
let animation = CAKeyframeAnimation(keyPath: "opacity") | |
animation.keyTimes = [0, 0.5, 1] | |
animation.timingFunctions = [timingFunction, timingFunction] | |
animation.values = [1, 0.3, 1] | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: lineWidth, height: lineHeight), cornerRadius: lineWidth/2) | |
for i in 0..<8 { | |
let angle = .pi / 4 * CGFloat(i) | |
let line = CAShapeLayer() | |
line.frame = CGRect(x: (containerSize-lineWidth)/2, y: (containerSize-lineHeight)/2, width: lineWidth, height: lineHeight) | |
line.path = path.cgPath | |
line.backgroundColor = nil | |
line.fillColor = colorAnimation.cgColor | |
let container = CALayer() | |
container.frame = CGRect(x: radius * (cos(angle) + 1), y: radius * (sin(angle) + 1), width: containerSize, height: containerSize) | |
container.addSublayer(line) | |
container.sublayerTransform = CATransform3DMakeRotation(.pi / 2 + angle, 0, 0, 1) | |
animation.beginTime = beginTime - beginTimes[i] | |
container.add(animation, forKey: "animation") | |
view.layer.addSublayer(container) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationCircleRotateChase(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let spacing: CGFloat = 3 | |
let radius = (width - 4 * spacing) / 3.5 | |
let radiusX = (width - radius) / 2 | |
let duration: CFTimeInterval = 1.5 | |
let path = UIBezierPath(arcCenter: CGPoint(x: radius/2, y: radius/2), radius: radius/2, startAngle: 0, endAngle: 2 * .pi, clockwise: false) | |
let pathPosition = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: radiusX, startAngle: 1.5 * .pi, endAngle: 3.5 * .pi, clockwise: true) | |
for i in 0..<5 { | |
let rate = Float(i) * 1 / 5 | |
let fromScale = 1 - rate | |
let toScale = 0.2 + rate | |
let timeFunc = CAMediaTimingFunction(controlPoints: 0.5, 0.15 + rate, 0.25, 1) | |
let animationScale = CABasicAnimation(keyPath: "transform.scale") | |
animationScale.duration = duration | |
animationScale.repeatCount = HUGE | |
animationScale.fromValue = fromScale | |
animationScale.toValue = toScale | |
let animationPosition = CAKeyframeAnimation(keyPath: "position") | |
animationPosition.duration = duration | |
animationPosition.repeatCount = HUGE | |
animationPosition.path = pathPosition.cgPath | |
let animation = CAAnimationGroup() | |
animation.animations = [animationScale, animationPosition] | |
animation.timingFunction = timeFunc | |
animation.duration = duration | |
animation.repeatCount = HUGE | |
animation.isRemovedOnCompletion = false | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: radius, height: radius) | |
layer.path = path.cgPath | |
layer.fillColor = colorAnimation.cgColor | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animationCircleStrokeSpin(_ view: UIView) { | |
let width = view.frame.size.width | |
let height = view.frame.size.height | |
let beginTime: Double = 0.5 | |
let durationStart: Double = 1.2 | |
let durationStop: Double = 0.7 | |
let animationRotation = CABasicAnimation(keyPath: "transform.rotation") | |
animationRotation.byValue = 2 * Float.pi | |
animationRotation.timingFunction = CAMediaTimingFunction(name: .linear) | |
let animationStart = CABasicAnimation(keyPath: "strokeStart") | |
animationStart.duration = durationStart | |
animationStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.4, 0, 0.2, 1) | |
animationStart.fromValue = 0 | |
animationStart.toValue = 1 | |
animationStart.beginTime = beginTime | |
let animationStop = CABasicAnimation(keyPath: "strokeEnd") | |
animationStop.duration = durationStop | |
animationStop.timingFunction = CAMediaTimingFunction(controlPoints: 0.4, 0, 0.2, 1) | |
animationStop.fromValue = 0 | |
animationStop.toValue = 1 | |
let animation = CAAnimationGroup() | |
animation.animations = [animationRotation, animationStop, animationStart] | |
animation.duration = durationStart + beginTime | |
animation.repeatCount = .infinity | |
animation.isRemovedOnCompletion = false | |
animation.fillMode = .forwards | |
let path = UIBezierPath(arcCenter: CGPoint(x: width/2, y: height/2), radius: width/2, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) | |
let layer = CAShapeLayer() | |
layer.frame = CGRect(x: 0, y: 0, width: width, height: height) | |
layer.path = path.cgPath | |
layer.fillColor = nil | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 3 | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
// MARK: - Animated Icon | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animatedIconSucceed(_ view: UIView) { | |
let length = view.frame.width | |
let delay = (self.alpha == 0) ? 0.25 : 0.0 | |
let path = UIBezierPath() | |
path.move(to: CGPoint(x: length * 0.15, y: length * 0.50)) | |
path.addLine(to: CGPoint(x: length * 0.5, y: length * 0.80)) | |
path.addLine(to: CGPoint(x: length * 1.0, y: length * 0.25)) | |
let animation = CABasicAnimation(keyPath: "strokeEnd") | |
animation.duration = 0.25 | |
animation.fromValue = 0 | |
animation.toValue = 1 | |
animation.fillMode = .forwards | |
animation.isRemovedOnCompletion = false | |
animation.beginTime = CACurrentMediaTime() + delay | |
let layer = CAShapeLayer() | |
layer.path = path.cgPath | |
layer.fillColor = UIColor.clear.cgColor | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 9 | |
layer.lineCap = .round | |
layer.lineJoin = .round | |
layer.strokeEnd = 0 | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animatedIconFailed(_ view: UIView) { | |
let length = view.frame.width | |
let delay = (self.alpha == 0) ? 0.25 : 0.0 | |
let path1 = UIBezierPath() | |
let path2 = UIBezierPath() | |
path1.move(to: CGPoint(x: length * 0.15, y: length * 0.15)) | |
path2.move(to: CGPoint(x: length * 0.15, y: length * 0.85)) | |
path1.addLine(to: CGPoint(x: length * 0.85, y: length * 0.85)) | |
path2.addLine(to: CGPoint(x: length * 0.85, y: length * 0.15)) | |
let paths = [path1, path2] | |
let animation = CABasicAnimation(keyPath: "strokeEnd") | |
animation.duration = 0.15 | |
animation.fromValue = 0 | |
animation.toValue = 1 | |
animation.fillMode = .forwards | |
animation.isRemovedOnCompletion = false | |
for i in 0..<2 { | |
let layer = CAShapeLayer() | |
layer.path = paths[i].cgPath | |
layer.fillColor = UIColor.clear.cgColor | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 9 | |
layer.lineCap = .round | |
layer.lineJoin = .round | |
layer.strokeEnd = 0 | |
animation.beginTime = CACurrentMediaTime() + 0.25 * Double(i) + delay | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
private func animatedIconAdded(_ view: UIView) { | |
let length = view.frame.width | |
let delay = (self.alpha == 0) ? 0.25 : 0.0 | |
let path1 = UIBezierPath() | |
let path2 = UIBezierPath() | |
path1.move(to: CGPoint(x: length * 0.1, y: length * 0.5)) | |
path2.move(to: CGPoint(x: length * 0.5, y: length * 0.1)) | |
path1.addLine(to: CGPoint(x: length * 0.9, y: length * 0.5)) | |
path2.addLine(to: CGPoint(x: length * 0.5, y: length * 0.9)) | |
let paths = [path1, path2] | |
let animation = CABasicAnimation(keyPath: "strokeEnd") | |
animation.duration = 0.15 | |
animation.fromValue = 0 | |
animation.toValue = 1 | |
animation.fillMode = .forwards | |
animation.isRemovedOnCompletion = false | |
for i in 0..<2 { | |
let layer = CAShapeLayer() | |
layer.path = paths[i].cgPath | |
layer.fillColor = UIColor.clear.cgColor | |
layer.strokeColor = colorAnimation.cgColor | |
layer.lineWidth = 9 | |
layer.lineCap = .round | |
layer.lineJoin = .round | |
layer.strokeEnd = 0 | |
animation.beginTime = CACurrentMediaTime() + 0.25 * Double(i) + delay | |
layer.add(animation, forKey: "animation") | |
view.layer.addSublayer(layer) | |
} | |
} | |
} | |
// MARK: - ProgressView | |
//----------------------------------------------------------------------------------------------------------------------------------------------- | |
private class ProgressView: UIView { | |
var color: UIColor = .systemBackground { | |
didSet { setupLayers() } | |
} | |
private var progress: CGFloat = 0 | |
private var layerCircle = CAShapeLayer() | |
private var layerProgress = CAShapeLayer() | |
private var labelPercentage: UILabel = UILabel() | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
convenience init(_ color: UIColor) { | |
self.init(frame: .zero) | |
self.color = color | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
required init?(coder: NSCoder) { | |
super.init(coder: coder) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
override func draw(_ rect: CGRect) { | |
super.draw(rect) | |
setupLayers() | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
func setupLayers() { | |
subviews.forEach { $0.removeFromSuperview() } | |
layer.sublayers?.forEach { $0.removeFromSuperlayer() } | |
let width = frame.size.width | |
let height = frame.size.height | |
let center = CGPoint(x: width/2, y: height/2) | |
let radiusCircle = width / 2 | |
let radiusProgress = width / 2 - 5 | |
let pathCircle = UIBezierPath(arcCenter: center, radius: radiusCircle, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) | |
let pathProgress = UIBezierPath(arcCenter: center, radius: radiusProgress, startAngle: -0.5 * .pi, endAngle: 1.5 * .pi, clockwise: true) | |
layerCircle.path = pathCircle.cgPath | |
layerCircle.fillColor = UIColor.clear.cgColor | |
layerCircle.lineWidth = 3 | |
layerCircle.strokeColor = color.cgColor | |
layerProgress.path = pathProgress.cgPath | |
layerProgress.fillColor = UIColor.clear.cgColor | |
layerProgress.lineWidth = 7 | |
layerProgress.strokeColor = color.cgColor | |
layerProgress.strokeEnd = 0 | |
layer.addSublayer(layerCircle) | |
layer.addSublayer(layerProgress) | |
labelPercentage.frame = self.bounds | |
labelPercentage.textColor = color | |
labelPercentage.textAlignment = .center | |
addSubview(labelPercentage) | |
} | |
//------------------------------------------------------------------------------------------------------------------------------------------- | |
func setProgress(_ value: CGFloat, duration: TimeInterval = 0.2) { | |
let animation = CABasicAnimation(keyPath: "strokeEnd") | |
animation.duration = duration | |
animation.fromValue = progress | |
animation.toValue = value | |
animation.fillMode = .both | |
animation.isRemovedOnCompletion = false | |
layerProgress.add(animation, forKey: "animation") | |
progress = value | |
labelPercentage.text = "\(Int(value*100))%" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment