Merge branch 'feature/atomic_vds_loader' into feature/vds_batch_two
This commit is contained in:
commit
e3f8fe05bd
@ -590,6 +590,7 @@
|
|||||||
EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */; };
|
EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */; };
|
||||||
EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */; };
|
EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */; };
|
||||||
EAA0CFB3275E831E00D65EB0 /* DisableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */; };
|
EAA0CFB3275E831E00D65EB0 /* DisableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */; };
|
||||||
|
EAA482CE2B45F2F300978105 /* MFLoadingSpinner+VDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */; };
|
||||||
EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */; };
|
EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */; };
|
||||||
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */; };
|
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */; };
|
||||||
EAB14BC327D9378D0012AB2C /* RuleAnyModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */; };
|
EAB14BC327D9378D0012AB2C /* RuleAnyModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */; };
|
||||||
@ -1186,6 +1187,7 @@
|
|||||||
EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldEffectProtocol.swift; sourceTree = "<group>"; };
|
EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldEffectProtocol.swift; sourceTree = "<group>"; };
|
||||||
EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||||
EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||||
|
EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MFLoadingSpinner+VDS.swift"; sourceTree = "<group>"; };
|
||||||
EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSMoleculeViewProtocol.swift; sourceTree = "<group>"; };
|
EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSMoleculeViewProtocol.swift; sourceTree = "<group>"; };
|
||||||
EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleCompareModelProtocol.swift; sourceTree = "<group>"; };
|
EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleCompareModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyModelProtocol.swift; sourceTree = "<group>"; };
|
EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
@ -1641,6 +1643,7 @@
|
|||||||
D20492A324329A2800A5EED6 /* MVMCoreUIPagingProtocol.h */,
|
D20492A324329A2800A5EED6 /* MVMCoreUIPagingProtocol.h */,
|
||||||
D29DF2B121E7B76C003B2FB9 /* MFLoadingSpinner.h */,
|
D29DF2B121E7B76C003B2FB9 /* MFLoadingSpinner.h */,
|
||||||
D29DF2B221E7B76D003B2FB9 /* MFLoadingSpinner.m */,
|
D29DF2B221E7B76D003B2FB9 /* MFLoadingSpinner.m */,
|
||||||
|
EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */,
|
||||||
D29DF25821E6A22D003B2FB9 /* MFButtonProtocol.h */,
|
D29DF25821E6A22D003B2FB9 /* MFButtonProtocol.h */,
|
||||||
D29DF16B21E69E1F003B2FB9 /* ButtonDelegateProtocol.h */,
|
D29DF16B21E69E1F003B2FB9 /* ButtonDelegateProtocol.h */,
|
||||||
);
|
);
|
||||||
@ -2698,6 +2701,7 @@
|
|||||||
0A6682A42434DB8D00AD3CA1 /* ListLeftVariableRadioButtonBodyTextModel.swift in Sources */,
|
0A6682A42434DB8D00AD3CA1 /* ListLeftVariableRadioButtonBodyTextModel.swift in Sources */,
|
||||||
AA2AD116244EE46800BBFFE3 /* ListDeviceComplexLinkMedium.swift in Sources */,
|
AA2AD116244EE46800BBFFE3 /* ListDeviceComplexLinkMedium.swift in Sources */,
|
||||||
AA7F32AD246C0F8C00C965BA /* ListLeftVariableRadioButtonAllTextAndLinks.swift in Sources */,
|
AA7F32AD246C0F8C00C965BA /* ListLeftVariableRadioButtonAllTextAndLinks.swift in Sources */,
|
||||||
|
EAA482CE2B45F2F300978105 /* MFLoadingSpinner+VDS.swift in Sources */,
|
||||||
D272F5F92473163100BD1A8F /* BarButtonItem.swift in Sources */,
|
D272F5F92473163100BD1A8F /* BarButtonItem.swift in Sources */,
|
||||||
D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */,
|
D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */,
|
||||||
0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */,
|
0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */,
|
||||||
|
|||||||
@ -7,207 +7,30 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
open class LoadingSpinner: VDS.Loader, VDSMoleculeViewProtocol {
|
||||||
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
|
// MARK: - Public Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open var viewModel: LoadingSpinnerModel!
|
||||||
public var heightConstraint: NSLayoutConstraint?
|
open var delegateObject: MVMCoreUIDelegateObject?
|
||||||
public var widthConstraint: NSLayoutConstraint?
|
open var additionalData: [AnyHashable : Any]?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Lifecycle
|
// MARK: - Public Functions
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open func viewModelDidUpdate() {
|
||||||
override open var layer: CAShapeLayer {
|
size = Int(viewModel.diameter)
|
||||||
get { return super.layer as! CAShapeLayer }
|
surface = viewModel.surface
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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
|
// MARK: - MVMCoreViewProtocol
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 40.0 }
|
||||||
|
|
||||||
override open func didMoveToWindow() {
|
open func updateView(_ size: CGFloat) { }
|
||||||
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 Foundation
|
||||||
import MVMCore
|
import MVMCore
|
||||||
|
import VDS
|
||||||
|
|
||||||
open class LoadingSpinnerModel: MoleculeModelProtocol {
|
open class LoadingSpinnerModel: MoleculeModelProtocol {
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -17,8 +18,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
|||||||
public var id: String = UUID().uuidString
|
public var id: String = UUID().uuidString
|
||||||
|
|
||||||
public var backgroundColor: Color?
|
public var backgroundColor: Color?
|
||||||
public var strokeColor = Color(uiColor: .mvmBlack)
|
public var inverted: Bool = false
|
||||||
public var lineWidth: CGFloat = 4
|
|
||||||
public var diameter: CGFloat = 40
|
public var diameter: CGFloat = 40
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -28,10 +28,9 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
|||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case moleculeName
|
case moleculeName
|
||||||
case backgroundColor
|
|
||||||
case strokeColor
|
case strokeColor
|
||||||
case lineWidth
|
|
||||||
case diameter
|
case diameter
|
||||||
|
case inverted
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -48,18 +47,17 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
|||||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
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) {
|
if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) {
|
||||||
self.diameter = diameter
|
self.diameter = diameter
|
||||||
}
|
}
|
||||||
|
|
||||||
if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) {
|
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
|
||||||
self.strokeColor = strokeColor
|
self.inverted = inverted
|
||||||
}
|
}
|
||||||
|
|
||||||
if let lineWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .lineWidth) {
|
if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) {
|
||||||
self.lineWidth = lineWidth
|
self.inverted = !strokeColor.uiColor.isDark()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,9 +65,11 @@ open class LoadingSpinnerModel: MoleculeModelProtocol {
|
|||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(id, forKey: .id)
|
try container.encode(id, forKey: .id)
|
||||||
try container.encode(moleculeName, forKey: .moleculeName)
|
try container.encode(moleculeName, forKey: .moleculeName)
|
||||||
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
|
||||||
try container.encodeIfPresent(diameter, forKey: .diameter)
|
try container.encodeIfPresent(diameter, forKey: .diameter)
|
||||||
try container.encode(strokeColor, forKey: .strokeColor)
|
try container.encodeIfPresent(inverted, forKey: .inverted)
|
||||||
try container.encode(lineWidth, forKey: .lineWidth)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LoadingSpinnerModel {
|
||||||
|
public var surface: Surface { inverted ? .dark : .light }
|
||||||
|
}
|
||||||
|
|||||||
41
MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift
Normal file
41
MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// MFLoadingSpinner+VDS.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 1/3/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
extension MFLoadingSpinner {
|
||||||
|
var loader: Loader? {
|
||||||
|
subviews.first as? Loader
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc open func setSurface(_ strokeColor: UIColor?) {
|
||||||
|
if let strokeColor {
|
||||||
|
loader?.surface = strokeColor.isDark() ? .light : .dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc open func pause() {
|
||||||
|
loader?.isActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc open func resume() {
|
||||||
|
loader?.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc open func resumeAfterDelay() {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
|
self?.loader?.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc open func pin() -> NSDictionary? {
|
||||||
|
guard let size = loader?.size else { return nil }
|
||||||
|
return NSLayoutConstraint.constraintPinView(self, heightConstraint: true, heightConstant: CGFloat(size), widthConstraint: true, widthConstant: CGFloat(size)) as NSDictionary?
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,143 +7,52 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "MFLoadingSpinner.h"
|
#import "MFLoadingSpinner.h"
|
||||||
#import "UIColor+MFConvenience.h"
|
#import <VDS/VDS.h>
|
||||||
#import "NSLayoutConstraint+MFConvenience.h"
|
#import <MVMCoreUI/MVMCoreUI-Swift.h>
|
||||||
|
|
||||||
@interface MFLoadingSpinner ()
|
@interface MFLoadingSpinner ()
|
||||||
|
@property (strong, nonatomic) VDSLoader *loader;
|
||||||
@property (strong, nonatomic) CAShapeLayer *myCircle;
|
|
||||||
@property (strong, nonatomic) CADisplayLink *myDisplay;
|
|
||||||
@property (weak, nonatomic) dispatch_block_t resumeBlock;
|
|
||||||
|
|
||||||
@property (nonatomic) double prevFrame;
|
|
||||||
|
|
||||||
@property (nonatomic) BOOL isFast;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation MFLoadingSpinner
|
@implementation MFLoadingSpinner
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame
|
||||||
|
{
|
||||||
|
self = [super initWithFrame:frame];
|
||||||
const float radius = 19;
|
if (self) {
|
||||||
const float lineWidth = 3.0;
|
self.loader = [[VDSLoader alloc] init];
|
||||||
const float slowSpeed = 0.5;
|
[self addSubview: self.loader];
|
||||||
const float fastSpeed = 2.0;
|
[NSLayoutConstraint pinViewToSuperview:self.loader useMargins:false];
|
||||||
const float startSpeed = 1.0;
|
}
|
||||||
const float fastDistance = .45;
|
return self;
|
||||||
const float slowDistance = 0.1;
|
|
||||||
|
|
||||||
|
|
||||||
-(void)finalize {
|
|
||||||
[self.myDisplay invalidate];
|
|
||||||
self.myDisplay = nil;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)setUpCircle {
|
-(void)setUpCircle {
|
||||||
[self setUpCircle:[UIColor blackColor]];
|
[self setSurface: UIColor.blackColor];
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)setUpCircle:(UIColor *)strokeColor {
|
-(void)setUpCircle:(nullable UIColor *)strokeColor {
|
||||||
if(self.myCircle)
|
[self setSurface: strokeColor];
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CAShapeLayer *circle = [CAShapeLayer layer];
|
|
||||||
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius + lineWidth/2, radius + lineWidth/2) radius:radius startAngle:-M_PI_2 endAngle:3.5*M_PI clockwise:YES].CGPath;
|
|
||||||
circle.lineWidth = lineWidth;
|
|
||||||
circle.fillColor = [UIColor clearColor].CGColor;
|
|
||||||
circle.strokeColor = strokeColor.CGColor;
|
|
||||||
circle.lineCap = kCALineCapButt;
|
|
||||||
circle.strokeStart = 0;
|
|
||||||
circle.strokeEnd = 0+.05;
|
|
||||||
[self.layer addSublayer:circle];
|
|
||||||
self.myCircle = circle;
|
|
||||||
|
|
||||||
self.isFast = YES;
|
|
||||||
|
|
||||||
NSMutableDictionary *newActions = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"strokeStart",
|
|
||||||
[NSNull null], @"strokeEnd",
|
|
||||||
[NSNull null], @"strokeColor",
|
|
||||||
nil];
|
|
||||||
circle.actions = newActions;
|
|
||||||
|
|
||||||
self.myDisplay = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateSpinner)];
|
|
||||||
[self.myDisplay addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
||||||
self.myDisplay.frameInterval = 2;
|
|
||||||
self.prevFrame = CACurrentMediaTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)changeColor:(UIColor *)strokeColor {
|
-(void)changeColor:(nullable UIColor *)strokeColor {
|
||||||
self.myCircle.strokeColor = strokeColor.CGColor;
|
[self setSurface: strokeColor];
|
||||||
}
|
|
||||||
|
|
||||||
-(void)updateSpinner {
|
|
||||||
double currentTime = CACurrentMediaTime();
|
|
||||||
double renderTime = currentTime - self.prevFrame;
|
|
||||||
self.prevFrame = currentTime;
|
|
||||||
|
|
||||||
if(self.myCircle.strokeStart > 0.5 && self.myCircle.strokeEnd > 0.5) {
|
|
||||||
self.myCircle.strokeStart -= 0.5;
|
|
||||||
self.myCircle.strokeEnd -= 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distanceToStart = self.myCircle.strokeEnd - self.myCircle.strokeStart;
|
|
||||||
if(distanceToStart < slowDistance && !self.isFast) {
|
|
||||||
self.isFast = YES;
|
|
||||||
}
|
|
||||||
else if(distanceToStart > fastDistance && self.isFast) {
|
|
||||||
self.isFast = NO;
|
|
||||||
}
|
|
||||||
self.myCircle.strokeEnd += (self.isFast ? fastSpeed : slowSpeed) * renderTime;
|
|
||||||
self.myCircle.strokeStart+= startSpeed * renderTime;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)pauseSpinner {
|
- (void)pauseSpinner {
|
||||||
if (self.resumeBlock) {
|
[self pause];
|
||||||
// Cancel the current resume block if it hasn't run. dispatch our pause into the same queue incase the resume block is already running.
|
|
||||||
dispatch_block_cancel(self.resumeBlock);
|
|
||||||
self.resumeBlock = nil;
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
weakSelf.myDisplay.paused = YES;
|
|
||||||
weakSelf.hidden = YES;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.myDisplay.paused = YES;
|
|
||||||
self.hidden = YES;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)resumeSpinner {
|
- (void)resumeSpinner {
|
||||||
self.hidden = NO;
|
[self resume];
|
||||||
if (!self.myCircle) {
|
|
||||||
[self setUpCircle];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.myDisplay.paused = NO;
|
|
||||||
self.prevFrame = CACurrentMediaTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Starts the spinner after a slight delay.
|
||||||
- (void)resumeSpinnerAfterDelay {
|
- (void)resumeSpinnerAfterDelay {
|
||||||
if (!self.resumeBlock) {
|
[self resumeAfterDelay];
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
dispatch_block_t resume = dispatch_block_create(0, ^{
|
|
||||||
[weakSelf resumeSpinner];
|
|
||||||
weakSelf.resumeBlock = nil;
|
|
||||||
});
|
|
||||||
self.resumeBlock = resume;
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), resume);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable NSDictionary *)pinWidthAndHeight {
|
- (nullable NSDictionary *)pinWidthAndHeight {
|
||||||
CGFloat diameter = radius*2 + lineWidth;
|
return [self pin];
|
||||||
return [NSLayoutConstraint constraintPinView:self heightConstraint:YES heightConstant:diameter widthConstraint:YES widthConstant:diameter];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user