// // ActivityIndicator.swift // PrivadoVPN // // Created by Juraldinio on 2/28/21. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation import AppKit import QuartzCore @IBDesignable open class ActivityIndicator: NSView { private enum Constants { static let spinAnimationKey = "spinAnimation" static let progressPropertyKey = "progress" // static let animationKeyPath = "transform" } // MARK: - Properties /// Indicates if the view will show the progress, or just spin. @IBInspectable var isIndeterminate: Bool = true { didSet { if !self.isIndeterminate { // self.animates = NO; } } } /// The amount that should be shown when `isIndeterminate` is set to `YES`. @IBInspectable var progress: CGFloat = 0 { didSet { guard self.progress > 0 , self.progress <= 1 else { self.progress = oldValue return } if !self.isIndeterminate { self.reloadIndicatorContent() } } } /// Indicates if the view is animating. @IBInspectable var animates: Bool = true { didSet { self.reloadIndicatorContent() self.reloadAnimation() self.reloadVisibility() } } /// Indicates if the view will be hidden if it's stopped. @IBInspectable var hideWhenStopped: Bool = true { didSet { self.reloadVisibility() } } /// The length of a single line. @IBInspectable var lengthOfLine: CGFloat = 6.0 { didSet { self.reloadIndicatorContent() } } /// The width of a single line. @IBInspectable var widthOfLine: CGFloat = 3.0 { didSet { self.reloadIndicatorContent() } } /// The number of lines of the indicator. @IBInspectable var numberOfLines: Int = 8 { didSet { self.reloadIndicatorContent() self.reloadAnimation() } } /// The distance of the lines from the middle. @IBInspectable var innerMargin: CGFloat = 4.0 { didSet { self.reloadIndicatorContent() } } /// Duration of a single rotation. @IBInspectable // swiftlint:disable valid_ibinspectable var animationDuration: TimeInterval = 0.6 { didSet { self.reloadAnimation() } } // swiftlint:enable valid_ibinspectable /// Defines if the animation is smooth or gradual. @IBInspectable var steppedAnimation: Bool = true { didSet { self.reloadAnimation() } } /// The color of the progress indicator @IBInspectable var color: NSColor = .black { didSet { self.reloadIndicatorContent() } } private lazy var rootLayer: CALayer = { let layer = CALayer() return layer }() private lazy var progressIndicatorLayer: CALayer = { let layer = CALayer() return layer }() // MARK: - Initializers override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.initLayers() } public required init?(coder: NSCoder) { super.init(coder: coder) self.initLayers() } // MARK: - Lifecycle open override func awakeFromNib() { super.awakeFromNib() self.reloadAnimation() } static public override func defaultAnimation(forKey key: NSAnimatablePropertyKey) -> Any? { guard key.compare(Constants.progressPropertyKey) == .orderedSame else { return super.defaultAnimation(forKey: key) } return CABasicAnimation() } // MARK: - Private private func initLayers() { self.layer = self.rootLayer self.wantsLayer = true self.progressIndicatorLayer.frame = self.rootLayer.frame self.rootLayer.addSublayer(self.progressIndicatorLayer) self.reloadIndicatorContent() self.reloadAnimation() } private func reloadAnimation() { self.progressIndicatorLayer.removeAnimation(forKey: Constants.spinAnimationKey) if self.animates { self.progressIndicatorLayer.add(self.keyFrameAnimationForCurrentPreferences(), forKey: Constants.spinAnimationKey) } } private func reloadIndicatorContent() { self.progressIndicatorLayer.contents = self.progressImage } private func reloadVisibility() { if self.hideWhenStopped , !self.animates , self.isIndeterminate { self.isHidden = true } else { self.isHidden = false } } // MARK: - Drawing var progressImage: NSImage { guard self.bounds.size.width > 0 , self.bounds.size.height > 0 else { return NSImage() } let progressImage = NSImage(size: self.bounds.size) progressImage.lockFocus() do { NSGraphicsContext.saveGraphicsState() do { self.color.set() let rect = self.bounds let line = NSBezierPath(roundedRect: CGRect(x: rect.width / 2 - self.widthOfLine / 2, y: rect.height / 2 - self.innerMargin - self.lengthOfLine, width: self.widthOfLine, height: self.lengthOfLine), xRadius: self.widthOfLine / 2, yRadius: self.widthOfLine / 2) let lineDrawingBlock: (Int) -> Void = { lineNumber in let lineInstance = line.rotate(angle: CGFloat((2.0 * Double.pi) / Double(self.numberOfLines * lineNumber) + Double.pi), at: CGPoint(x: rect.width / 2, y: rect.height / 2)) if self.isIndeterminate { self.color .withAlphaComponent( 1.0 - CGFloat(1.0 / CGFloat(self.numberOfLines * lineNumber)) ) .set() } lineInstance.fill() } let ranges: ClosedRange = !self.isIndeterminate ? self.numberOfLines - Int(CGFloat(self.numberOfLines) * self.progress) ... self.numberOfLines : 0 ... self.numberOfLines - 1 for index in ranges { lineDrawingBlock(index) } } NSGraphicsContext.restoreGraphicsState() } progressImage.unlockFocus() return progressImage } // MARK: - Animation method /// Override this method to achieve a custom animation. /// - Returns: animation which will be put on the progress indicator layer. open func keyFrameAnimationForCurrentPreferences() -> CAKeyframeAnimation { var keyFrameValues = [NSNumber]() var keyTimeValues = [NSNumber]() if self.steppedAnimation { keyFrameValues.append(0.0) for index in 0 ..< self.numberOfLines { let value = NSNumber(value: ((-1.0) * Double.pi * (2.0 / Double(self.numberOfLines * index)))) keyFrameValues.append( value ) keyFrameValues.append( value ) } keyFrameValues.append( NSNumber(value: (-Double.pi * 2.0)) ) keyTimeValues.append(0.0) for index in 0 ..< (self.numberOfLines - 1) { keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * index))) ) keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * (index + 1)))) ) } keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * (self.numberOfLines - 1)))) ) } else { keyFrameValues.append( NSNumber(value: (-Double.pi * 0.0)) ) keyFrameValues.append( NSNumber(value: (-Double.pi * 0.5)) ) keyFrameValues.append( NSNumber(value: (-Double.pi * 1.0)) ) keyFrameValues.append( NSNumber(value: (-Double.pi * 1.5)) ) keyFrameValues.append( NSNumber(value: (-Double.pi * 2.0)) ) } let animation = CAKeyframeAnimation(keyPath: Constants.animationKeyPath) animation.repeatCount = .greatestFiniteMagnitude animation.values = keyFrameValues animation.keyTimes = keyTimeValues animation.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) animation.duration = self.animationDuration animation.beginTime = 1 return animation } }