From 6a8e45fc9e2290cabb94643c3b9f895788a61446 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 6 Dec 2023 14:39:46 -0600 Subject: [PATCH 1/3] 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 } +} From c03e04069abe60053ded75da63cb58134ed4a0b9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 3 Jan 2024 14:04:30 -0600 Subject: [PATCH 2/3] refactored loader Signed-off-by: Matt Bruce --- MVMCoreUI.xcodeproj/project.pbxproj | 10 +- .../Atomic/Atoms/Views/LoadImageView.swift | 6 +- MVMCoreUI/Atomic/Atoms/Views/WebView.swift | 4 +- .../Legacy/Views/MFLoadingSpinner+VDS.swift | 42 ++++++ MVMCoreUI/Legacy/Views/MFLoadingSpinner.h | 11 -- MVMCoreUI/Legacy/Views/MFLoadingSpinner.m | 138 ++---------------- 6 files changed, 68 insertions(+), 143 deletions(-) create mode 100644 MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 8619ce46..09c333cb 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -298,8 +298,8 @@ AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */; }; AFE4A1D127DFB5EE00C458D0 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */; }; AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */; }; - B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBE29DF34730005D28B /* BadgeModel.swift */; }; B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBC29DF34680005D28B /* Badge.swift */; }; + B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBE29DF34730005D28B /* BadgeModel.swift */; }; BB105859248DEFF70069D008 /* UICollectionViewLeftAlignedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */; }; BB1D17E0244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */; }; BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */; }; @@ -576,8 +576,8 @@ EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; }; EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */; }; - EA985C402970939A00F2FF2E /* TileletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3F2970939A00F2FF2E /* TileletModel.swift */; }; EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilelet.swift */; }; + EA985C402970939A00F2FF2E /* TileletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3F2970939A00F2FF2E /* TileletModel.swift */; }; EA985C602970A3F000F2FF2E /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C5F2970A3F000F2FF2E /* VDS.framework */; }; EA985C642970A40E00F2FF2E /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C632970A40E00F2FF2E /* VDSTypographyTokens.xcframework */; }; EA985C852981AA9C00F2FF2E /* VDS-Enums+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C842981AA9C00F2FF2E /* VDS-Enums+Codable.swift */; }; @@ -587,6 +587,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 */; }; @@ -890,8 +891,8 @@ AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = ""; }; AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = ""; }; AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; - B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = ""; }; B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; + B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = ""; }; BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewLeftAlignedLayout.swift; sourceTree = ""; }; BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMediumModel.swift; sourceTree = ""; }; BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMedium.swift; sourceTree = ""; }; @@ -1180,6 +1181,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 = ""; }; @@ -1635,6 +1637,7 @@ D20492A324329A2800A5EED6 /* MVMCoreUIPagingProtocol.h */, D29DF2B121E7B76C003B2FB9 /* MFLoadingSpinner.h */, D29DF2B221E7B76D003B2FB9 /* MFLoadingSpinner.m */, + EAA482CD2B45F2F300978105 /* MFLoadingSpinner+VDS.swift */, D29DF25821E6A22D003B2FB9 /* MFButtonProtocol.h */, D29DF16B21E69E1F003B2FB9 /* ButtonDelegateProtocol.h */, ); @@ -2689,6 +2692,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/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index 780f8829..e7601015 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -117,7 +117,7 @@ spinnerHeight = constraint.constant loadingSpinnerHeightConstraint?.constant = 0 loadingSpinnerHeightConstraint?.isActive = true - loadingSpinner.pause() + loadingSpinner.pauseSpinner() } } @@ -326,7 +326,7 @@ guard let self = self, let loadingImageName = self.currentImageName, loadingImageName == imageName else { return } self.isFallbackImage = isFallbackImage - self.loadingSpinner.pause() + self.loadingSpinner.pauseSpinner() let layoutWillChange = self.shouldNotifyDelegateOnUpdate ? self.layoutWillChange(width: self.currentImageWidth, height: self.currentImageHeight, size: image?.size) : false self.addConstraints(width: width, height: height, size: image?.size) self.loadingSpinnerHeightConstraint?.constant = 0 @@ -359,7 +359,7 @@ return } self?.loadingSpinnerHeightConstraint?.constant = 0 - self?.loadingSpinner.pause() + self?.loadingSpinner.pauseSpinner() if flipImage, let cgImage = image.cgImage { self?.imageView.image = UIImage(cgImage: cgImage, scale: image.scale, orientation: UIImage.Orientation.upMirrored) } else { diff --git a/MVMCoreUI/Atomic/Atoms/Views/WebView.swift b/MVMCoreUI/Atomic/Atoms/Views/WebView.swift index d3a91920..8e2923be 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/WebView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/WebView.swift @@ -122,7 +122,7 @@ extension WebView : WKUIDelegate { public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { // hide loading overLayer.isHidden = true - loadingSpinner.pause() + loadingSpinner.pauseSpinner() //update webview's heigth when webview is ready if !dynamicHeight { @@ -159,7 +159,7 @@ extension WebView : WKUIDelegate { //actually no error handle page show in webview. We can handle the error display view by our self. //or stop loading by default overLayer.isHidden = true - loadingSpinner.pause() + loadingSpinner.pauseSpinner() } } diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift new file mode 100644 index 00000000..f058699c --- /dev/null +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift @@ -0,0 +1,42 @@ +// +// 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(setUpCircle:) + open func setUpCircle(strokeColor: UIColor?) { + if let strokeColor { + loader?.surface = strokeColor.isDark() ? .light : .dark + } + } + + @objc open func pauseSpinner() { + loader?.isActive = false + } + + @objc open func resumeSpinner() { + loader?.isActive = true + } + + @objc open func resumeSpinnerAfterDelay() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + self?.loader?.isActive = true + } + } + + @objc open func pinWidthAndHeight() -> 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.h b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h index 6a44f743..6e4bb55c 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h @@ -12,17 +12,6 @@ -(void)setUpCircle; --(void)setUpCircle:(nullable UIColor *)strokeColor; - -(void)changeColor:(nullable UIColor *)strokeColor; -- (void)pauseSpinner; - -- (void)resumeSpinner; - -// Starts the spinner after a slight delay. -- (void)resumeSpinnerAfterDelay; - -- (nullable NSDictionary *)pinWidthAndHeight; - @end diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m index 7b354dca..07a254d6 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m @@ -7,143 +7,33 @@ // #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]]; -} - --(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(); + [self setUpCircle:[UIColor blackColor]]; } -(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)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; - } -} - -- (void)resumeSpinner { - self.hidden = NO; - if (!self.myCircle) { - [self setUpCircle]; - return; - } - - self.myDisplay.paused = NO; - self.prevFrame = CACurrentMediaTime(); -} - -- (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); - } -} - -- (nullable NSDictionary *)pinWidthAndHeight { - CGFloat diameter = radius*2 + lineWidth; - return [NSLayoutConstraint constraintPinView:self heightConstraint:YES heightConstant:diameter widthConstraint:YES widthConstant:diameter]; + [self setUpCircle: strokeColor]; } @end From eb8ea989ec6e7e28727099ae0a4c27bb906be0ec Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 3 Jan 2024 15:13:39 -0600 Subject: [PATCH 3/3] refactored back in the obj-c interface to interact with swift extension Signed-off-by: Matt Bruce --- .../Atomic/Atoms/Views/LoadImageView.swift | 6 ++--- MVMCoreUI/Atomic/Atoms/Views/WebView.swift | 4 +-- .../Legacy/Views/MFLoadingSpinner+VDS.swift | 11 ++++---- MVMCoreUI/Legacy/Views/MFLoadingSpinner.h | 11 ++++++++ MVMCoreUI/Legacy/Views/MFLoadingSpinner.m | 27 ++++++++++++++++--- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index e7601015..780f8829 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -117,7 +117,7 @@ spinnerHeight = constraint.constant loadingSpinnerHeightConstraint?.constant = 0 loadingSpinnerHeightConstraint?.isActive = true - loadingSpinner.pauseSpinner() + loadingSpinner.pause() } } @@ -326,7 +326,7 @@ guard let self = self, let loadingImageName = self.currentImageName, loadingImageName == imageName else { return } self.isFallbackImage = isFallbackImage - self.loadingSpinner.pauseSpinner() + self.loadingSpinner.pause() let layoutWillChange = self.shouldNotifyDelegateOnUpdate ? self.layoutWillChange(width: self.currentImageWidth, height: self.currentImageHeight, size: image?.size) : false self.addConstraints(width: width, height: height, size: image?.size) self.loadingSpinnerHeightConstraint?.constant = 0 @@ -359,7 +359,7 @@ return } self?.loadingSpinnerHeightConstraint?.constant = 0 - self?.loadingSpinner.pauseSpinner() + self?.loadingSpinner.pause() if flipImage, let cgImage = image.cgImage { self?.imageView.image = UIImage(cgImage: cgImage, scale: image.scale, orientation: UIImage.Orientation.upMirrored) } else { diff --git a/MVMCoreUI/Atomic/Atoms/Views/WebView.swift b/MVMCoreUI/Atomic/Atoms/Views/WebView.swift index 8e2923be..d3a91920 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/WebView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/WebView.swift @@ -122,7 +122,7 @@ extension WebView : WKUIDelegate { public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { // hide loading overLayer.isHidden = true - loadingSpinner.pauseSpinner() + loadingSpinner.pause() //update webview's heigth when webview is ready if !dynamicHeight { @@ -159,7 +159,7 @@ extension WebView : WKUIDelegate { //actually no error handle page show in webview. We can handle the error display view by our self. //or stop loading by default overLayer.isHidden = true - loadingSpinner.pauseSpinner() + loadingSpinner.pause() } } diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift index f058699c..f58a1395 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner+VDS.swift @@ -14,28 +14,27 @@ extension MFLoadingSpinner { subviews.first as? Loader } - @objc(setUpCircle:) - open func setUpCircle(strokeColor: UIColor?) { + @objc open func setSurface(_ strokeColor: UIColor?) { if let strokeColor { loader?.surface = strokeColor.isDark() ? .light : .dark } } - @objc open func pauseSpinner() { + @objc open func pause() { loader?.isActive = false } - @objc open func resumeSpinner() { + @objc open func resume() { loader?.isActive = true } - @objc open func resumeSpinnerAfterDelay() { + @objc open func resumeAfterDelay() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.loader?.isActive = true } } - @objc open func pinWidthAndHeight() -> NSDictionary? { + @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.h b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h index 6e4bb55c..6a44f743 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.h @@ -12,6 +12,17 @@ -(void)setUpCircle; +-(void)setUpCircle:(nullable UIColor *)strokeColor; + -(void)changeColor:(nullable UIColor *)strokeColor; +- (void)pauseSpinner; + +- (void)resumeSpinner; + +// Starts the spinner after a slight delay. +- (void)resumeSpinnerAfterDelay; + +- (nullable NSDictionary *)pinWidthAndHeight; + @end diff --git a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m index 07a254d6..97a88518 100644 --- a/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m +++ b/MVMCoreUI/Legacy/Views/MFLoadingSpinner.m @@ -7,7 +7,6 @@ // #import "MFLoadingSpinner.h" -#import "NSLayoutConstraint+MFConvenience.h" #import #import @@ -29,11 +28,31 @@ } -(void)setUpCircle { - [self setUpCircle:[UIColor blackColor]]; + [self setSurface: UIColor.blackColor]; } --(void)changeColor:(UIColor *)strokeColor { - [self setUpCircle: strokeColor]; +-(void)setUpCircle:(nullable UIColor *)strokeColor { + [self setSurface: strokeColor]; } +-(void)changeColor:(nullable UIColor *)strokeColor { + [self setSurface: strokeColor]; +} + +- (void)pauseSpinner { + [self pause]; +} + +- (void)resumeSpinner { + [self resume]; +} + +// Starts the spinner after a slight delay. +- (void)resumeSpinnerAfterDelay { + [self resumeAfterDelay]; +} + +- (nullable NSDictionary *)pinWidthAndHeight { + return [self pin]; +} @end