initial cut for VDS Loader
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
aba99b887b
commit
6a8e45fc9e
@ -7,207 +7,30 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDS
|
||||
|
||||
open class LoadingSpinner: VDS.Loader, VDSMoleculeViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var viewModel: LoadingSpinnerModel!
|
||||
open var delegateObject: MVMCoreUIDelegateObject?
|
||||
open var additionalData: [AnyHashable : Any]?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Functions
|
||||
//--------------------------------------------------
|
||||
open func viewModelDidUpdate() {
|
||||
size = Int(viewModel.diameter)
|
||||
surface = viewModel.surface
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MVMCoreViewProtocol
|
||||
//--------------------------------------------------
|
||||
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 40.0 }
|
||||
|
||||
open class LoadingSpinner: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var strokeColor: UIColor = .mvmBlack
|
||||
|
||||
public var lineWidth: CGFloat = 4.0
|
||||
|
||||
public var speed: Float = 1.5
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
|
||||
public var heightConstraint: NSLayoutConstraint?
|
||||
public var widthConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
override open var layer: CAShapeLayer {
|
||||
get { return super.layer as! CAShapeLayer }
|
||||
}
|
||||
|
||||
override open class var layerClass: AnyClass {
|
||||
return CAShapeLayer.self
|
||||
}
|
||||
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 0)
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 0)
|
||||
}
|
||||
|
||||
override open func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
open func updateView(_ size: CGFloat) { }
|
||||
|
||||
layer.fillColor = nil
|
||||
layer.strokeColor = strokeColor.cgColor
|
||||
layer.lineWidth = lineWidth
|
||||
layer.lineCap = .butt
|
||||
layer.speed = speed
|
||||
let halfWidth = lineWidth / 2
|
||||
let radius = (bounds.width - lineWidth) / 2
|
||||
layer.path = UIBezierPath(arcCenter: CGPoint(x: radius + halfWidth,
|
||||
y: radius + halfWidth),
|
||||
radius: radius,
|
||||
startAngle: -CGFloat.pi / 2,
|
||||
endAngle: 2 * CGFloat.pi,
|
||||
clockwise: true).cgPath
|
||||
}
|
||||
|
||||
open override func updateView(_ size: CGFloat) {
|
||||
super.updateView(size)
|
||||
|
||||
layer.removeAllAnimations()
|
||||
animate()
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
|
||||
layer.removeAllAnimations()
|
||||
heightConstraint?.isActive = false
|
||||
widthConstraint?.isActive = false
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Animation
|
||||
//--------------------------------------------------
|
||||
|
||||
override open func didMoveToWindow() {
|
||||
animate()
|
||||
}
|
||||
|
||||
struct Pose {
|
||||
/// Delayed time (in seconds) to execute after the previous Pose.
|
||||
let delay: CFTimeInterval
|
||||
/// The time into the animation to begin drawing.
|
||||
let startTime: CGFloat
|
||||
/// The length of the drawn line.
|
||||
let length: CGFloat
|
||||
}
|
||||
|
||||
// TODO: This needs more attention to improve frame smoothness.
|
||||
class var poses: [Pose] {
|
||||
get {
|
||||
return [
|
||||
Pose(delay: 0.0, startTime: 0.000, length: 0.7),
|
||||
Pose(delay: 0.7, startTime: 0.500, length: 0.5),
|
||||
Pose(delay: 0.6, startTime: 1.000, length: 0.3),
|
||||
Pose(delay: 0.5, startTime: 1.500, length: 0.2),
|
||||
Pose(delay: 0.5, startTime: 1.875, length: 0.2),
|
||||
Pose(delay: 0.3, startTime: 2.250, length: 0.3),
|
||||
Pose(delay: 0.2, startTime: 2.600, length: 0.5),
|
||||
Pose(delay: 0.2, startTime: 3.000, length: 0.7)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private func animate() {
|
||||
var time: CFTimeInterval = 0
|
||||
var times = [CFTimeInterval]()
|
||||
var start: CGFloat = 0
|
||||
var rotations = [CGFloat]()
|
||||
var strokeEnds = [CGFloat]()
|
||||
|
||||
let poses = Self.poses
|
||||
var totalSeconds: CFTimeInterval = poses.reduce(0) { $0 + $1.delay }
|
||||
|
||||
for pose in poses {
|
||||
time += pose.delay
|
||||
times.append(time / totalSeconds)
|
||||
start = pose.startTime
|
||||
rotations.append(start * 2 * CGFloat.pi)
|
||||
strokeEnds.append(pose.length)
|
||||
}
|
||||
|
||||
totalSeconds += 0.3
|
||||
|
||||
animateKeyPath(keyPath: "strokeEnd", duration: totalSeconds, times: times, values: strokeEnds)
|
||||
animateKeyPath(keyPath: "transform.rotation", duration: totalSeconds, times: times, values: rotations)
|
||||
}
|
||||
|
||||
private func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [CGFloat]) {
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: keyPath)
|
||||
animation.keyTimes = times as [NSNumber]?
|
||||
animation.values = values
|
||||
animation.calculationMode = .linear
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.duration = duration
|
||||
animation.rotationMode = .rotateAuto
|
||||
animation.fillMode = .forwards
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.repeatCount = .infinity
|
||||
layer.add(animation, forKey: animation.keyPath)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
func resumeSpinnerAfterDelay() {
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.resumeAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
func pauseAnimations() {
|
||||
|
||||
let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil)
|
||||
layer.speed = 0
|
||||
isHidden = true
|
||||
layer.timeOffset = pausedTime
|
||||
}
|
||||
|
||||
func resumeAnimations() {
|
||||
|
||||
let pausedTime = layer.timeOffset
|
||||
isHidden = false
|
||||
layer.speed = speed
|
||||
layer.timeOffset = 0
|
||||
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
|
||||
layer.beginTime = timeSincePause
|
||||
}
|
||||
|
||||
func stopAllAnimations() {
|
||||
|
||||
layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
func pinWidthAndHeight(diameter: CGFloat) {
|
||||
|
||||
let dimension = diameter + lineWidth
|
||||
heightConstraint?.constant = dimension
|
||||
widthConstraint?.constant = dimension
|
||||
heightConstraint?.isActive = true
|
||||
widthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - MoleculeViewProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||
super.set(with: model, delegateObject, additionalData)
|
||||
guard let model = model as? LoadingSpinnerModel else { return }
|
||||
|
||||
strokeColor = model.strokeColor.uiColor
|
||||
lineWidth = model.lineWidth
|
||||
pinWidthAndHeight(diameter: model.diameter)
|
||||
}
|
||||
|
||||
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||||
return 40.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import MVMCore
|
||||
import VDS
|
||||
|
||||
open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
//--------------------------------------------------
|
||||
@ -17,8 +18,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
public var id: String = UUID().uuidString
|
||||
|
||||
public var backgroundColor: Color?
|
||||
public var strokeColor = Color(uiColor: .mvmBlack)
|
||||
public var lineWidth: CGFloat = 4
|
||||
public var inverted: Bool = false
|
||||
public var diameter: CGFloat = 40
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -28,10 +28,9 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case moleculeName
|
||||
case backgroundColor
|
||||
case strokeColor
|
||||
case lineWidth
|
||||
case diameter
|
||||
case inverted
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -48,18 +47,17 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
|
||||
|
||||
if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) {
|
||||
self.diameter = diameter
|
||||
}
|
||||
|
||||
if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) {
|
||||
self.strokeColor = strokeColor
|
||||
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||
self.inverted = inverted
|
||||
}
|
||||
|
||||
if let lineWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .lineWidth) {
|
||||
self.lineWidth = lineWidth
|
||||
if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) {
|
||||
self.inverted = !strokeColor.uiColor.isDark()
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,9 +65,11 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(moleculeName, forKey: .moleculeName)
|
||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||
try container.encodeIfPresent(diameter, forKey: .diameter)
|
||||
try container.encode(strokeColor, forKey: .strokeColor)
|
||||
try container.encode(lineWidth, forKey: .lineWidth)
|
||||
try container.encodeIfPresent(inverted, forKey: .inverted)
|
||||
}
|
||||
}
|
||||
|
||||
extension LoadingSpinnerModel {
|
||||
public var surface: Surface { inverted ? .dark : .light }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user