Files
2021-06-02 12:28:09 +03:00

303 lines
9.1 KiB
Swift

//
// 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
}
}