diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 0b5aea33..af42161b 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -590,6 +590,7 @@ EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */; }; EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.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 */; }; EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.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 = ""; }; EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideFormFieldEffectModel.swift; sourceTree = ""; }; EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableFormFieldEffectModel.swift; sourceTree = ""; }; + EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MFLoadingSpinner+VDS.swift"; sourceTree = ""; }; EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSMoleculeViewProtocol.swift; sourceTree = ""; }; EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleCompareModelProtocol.swift; sourceTree = ""; }; EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyModelProtocol.swift; sourceTree = ""; }; @@ -1641,6 +1643,7 @@ D20492A324329A2800A5EED6 /* MVMCoreUIPagingProtocol.h */, D29DF2B121E7B76C003B2FB9 /* MFLoadingSpinner.h */, D29DF2B221E7B76D003B2FB9 /* MFLoadingSpinner.m */, + EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */, D29DF25821E6A22D003B2FB9 /* MFButtonProtocol.h */, D29DF16B21E69E1F003B2FB9 /* ButtonDelegateProtocol.h */, ); @@ -2698,6 +2701,7 @@ 0A6682A42434DB8D00AD3CA1 /* ListLeftVariableRadioButtonBodyTextModel.swift in Sources */, AA2AD116244EE46800BBFFE3 /* ListDeviceComplexLinkMedium.swift in Sources */, AA7F32AD246C0F8C00C965BA /* ListLeftVariableRadioButtonAllTextAndLinks.swift in Sources */, + EAA482CE2B45F2F300978105 /* MFLoadingSpinner+VDS.swift in Sources */, D272F5F92473163100BD1A8F /* BarButtonItem.swift in Sources */, D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */, 0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */, 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 } +} diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift new file mode 100644 index 00000000..f58a1395 --- /dev/null +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift @@ -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? + } +} diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m index 7b354dca..97a88518 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m @@ -7,143 +7,52 @@ // #import "MFLoadingSpinner.h" -#import "UIColor+MFConvenience.h" -#import "NSLayoutConstraint+MFConvenience.h" +#import +#import @interface MFLoadingSpinner () - -@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; - +@property (strong, nonatomic) VDSLoader *loader; @end @implementation MFLoadingSpinner - - - -const float radius = 19; -const float lineWidth = 3.0; -const float slowSpeed = 0.5; -const float fastSpeed = 2.0; -const float startSpeed = 1.0; -const float fastDistance = .45; -const float slowDistance = 0.1; - - --(void)finalize { - [self.myDisplay invalidate]; - self.myDisplay = nil; +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.loader = [[VDSLoader alloc] init]; + [self addSubview: self.loader]; + [NSLayoutConstraint pinViewToSuperview:self.loader useMargins:false]; + } + return self; } -(void)setUpCircle { - [self setUpCircle:[UIColor blackColor]]; + [self setSurface: UIColor.blackColor]; } --(void)setUpCircle:(UIColor *)strokeColor { - if(self.myCircle) - { - 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)setUpCircle:(nullable UIColor *)strokeColor { + [self setSurface: strokeColor]; } --(void)changeColor:(UIColor *)strokeColor { - self.myCircle.strokeColor = strokeColor.CGColor; -} - --(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)changeColor:(nullable UIColor *)strokeColor { + [self setSurface: strokeColor]; } - (void)pauseSpinner { - if (self.resumeBlock) { - // 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; - } + [self pause]; } - (void)resumeSpinner { - self.hidden = NO; - if (!self.myCircle) { - [self setUpCircle]; - return; - } - - self.myDisplay.paused = NO; - self.prevFrame = CACurrentMediaTime(); + [self resume]; } +// Starts the spinner after a slight delay. - (void)resumeSpinnerAfterDelay { - if (!self.resumeBlock) { - __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); - } + [self resumeAfterDelay]; } - (nullable NSDictionary *)pinWidthAndHeight { - CGFloat diameter = radius*2 + lineWidth; - return [NSLayoutConstraint constraintPinView:self heightConstraint:YES heightConstant:diameter widthConstraint:YES widthConstant:diameter]; + return [self pin]; } - @end