From 6a8e45fc9e2290cabb94643c3b9f895788a61446 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 6 Dec 2023 14:39:46 -0600 Subject: [PATCH] initial cut for VDS Loader Signed-off-by: Matt Bruce --- .../Atomic/Atoms/Views/LoadingSpinner.swift | 223 ++---------------- .../Atoms/Views/LoadingSpinnerModel.swift | 24 +- 2 files changed, 35 insertions(+), 212 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinner.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinner.swift index 2d58c8f4..04128e90 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinner.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinner.swift @@ -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 - } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift index b74807fb..6959bc8f 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift @@ -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 } +}