From 31506606610a928c51c04a67f961ea615779e7ed Mon Sep 17 00:00:00 2001 From: OmarLabib Date: Mon, 24 May 2021 16:55:07 +0200 Subject: [PATCH] fix: fix loader issue fix the issue that was happening if stopAnimating was called right after startAnimating. --- .../Classes/Behaviors/BaseBehavior.swift | 13 ++- .../Behaviors/CentreLoadingBehavior.swift | 104 +++++++++++------- .../Behaviors/EdgeLoadingBehavior.swift | 35 ++++-- 3 files changed, 96 insertions(+), 56 deletions(-) diff --git a/SpinningButton/Classes/Behaviors/BaseBehavior.swift b/SpinningButton/Classes/Behaviors/BaseBehavior.swift index 626a5cb..50aa760 100644 --- a/SpinningButton/Classes/Behaviors/BaseBehavior.swift +++ b/SpinningButton/Classes/Behaviors/BaseBehavior.swift @@ -14,16 +14,17 @@ public enum LoaderPosition: Equatable { } class BaseBehavior { - + weak var button: SpinningButton? - init(button: SpinningButton) { self.button = button } - - func startAnimating() {} - func stopAnimating() {} - + + func startAnimating() { + } + func stopAnimating() { + } + } extension BaseBehavior { diff --git a/SpinningButton/Classes/Behaviors/CentreLoadingBehavior.swift b/SpinningButton/Classes/Behaviors/CentreLoadingBehavior.swift index 5430a9c..ec5d52b 100644 --- a/SpinningButton/Classes/Behaviors/CentreLoadingBehavior.swift +++ b/SpinningButton/Classes/Behaviors/CentreLoadingBehavior.swift @@ -8,31 +8,33 @@ import UIKit class CentreLoadingBehavior: BaseBehavior { - + private var shouldShrinkOnLoading: Bool private var cachedTitle: String? = "" private var cachedCornerRadius: CGFloat = 0.0 private var cachedImage: UIImage? - + private var fullyShrunk = false + private var animationCompleted = false + init(button: SpinningButton, shrinkOnLoading: Bool = false) { self.shouldShrinkOnLoading = shrinkOnLoading super.init(button: button) } - + override func startAnimating() { - guard let button = button else {return} - + guard let button = button else { return } + setupConstraint() cacheState() button.clearTitleAndImage() animateSpinnerAndShrinkButton() } - + private func setupConstraint() { guard let button = button else { return } button.addSubview(button.loadingSpinner) button.loadingSpinner.centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true - + if shouldShrinkOnLoading { let offset = (button.frame.height - 20) / 2 button.loadingSpinner.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: offset).isActive = true @@ -40,47 +42,68 @@ class CentreLoadingBehavior: BaseBehavior { button.loadingSpinner.centerXAnchor.constraint(equalTo: button.centerXAnchor).isActive = true } } - + private func cacheState() { self.cachedTitle = button?.title(for: .normal) self.cachedImage = button?.image(for: .normal) self.cachedCornerRadius = button?.layer.cornerRadius ?? 0 } - + private func animateSpinnerAndShrinkButton() { + animationCompleted = false + fullyShrunk = false + button?.loadingSpinner.transform = CGAffineTransform(scaleX: 0, y: 0) - UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.45, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { - self.button?.loadingSpinner.transform = CGAffineTransform(scaleX: 1, y: 1) - }) { _ in + UIView.animate(withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 0.45, + initialSpringVelocity: 0, + options: .curveEaseInOut, + animations: { + self.button?.loadingSpinner.transform = CGAffineTransform(scaleX: 1, y: 1) + }) { _ in self.button?.loadingSpinner.startAnimating() - + self.animationCompleted = true if self.shouldShrinkOnLoading { - self.button?.animateShrinking() + self.button?.animateShrinking(completion: { + self.fullyShrunk = true + }) } } + } - + override func stopAnimating() { - if button?.loadingSpinner.isAnimating != true { - return + let delay = animationCompleted ? 0 : 0.9 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.button?.loadingSpinner.stopAnimating() + self.restoreStateOfShrinkingButton() + self.restoreStateOfNormalButton() } - button?.loadingSpinner.stopAnimating() - if shouldShrinkOnLoading { - button?.animateToOriginalWidth(completion: restoreCachedState) - } else { - restoreCachedState() + } + private func restoreStateOfShrinkingButton() { + guard self.shouldShrinkOnLoading else { return } + + let delay = self.fullyShrunk ? 0 : 0.5 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.button?.animateToOriginalWidth(completion: self.restoreCachedState) } } - + private func restoreStateOfNormalButton() { + guard !self.shouldShrinkOnLoading else { return } + + self.button?.isUserInteractionEnabled = true + self.restoreCachedState() + } + private func restoreCachedState() { button?.setTitle(cachedTitle, for: .normal) button?.setImage(cachedImage, for: .normal) UIView.animate(withDuration: 0.15) { self.button?.layer.cornerRadius = self.cachedCornerRadius } - } - + } private let shrinkCurve: CAMediaTimingFunction = CAMediaTimingFunction(name: .linear) @@ -88,21 +111,21 @@ private let expandCurve: CAMediaTimingFunction = CAMediaTimingFunction(controlPo private let shrinkDuration: CFTimeInterval = 0.25 private extension UIButton { - + func clearTitleAndImage() { setTitle(nil, for: .normal) setImage(nil, for: .normal) } - - func animateShrinking() { + + func animateShrinking(completion:(() -> Void)?) { UIView.animate(withDuration: 0.1, animations: { () -> Void in self.layer.cornerRadius = self.frame.height / 2 }, completion: { _ -> Void in - self.shrink() + self.shrink(completion: completion) }) } - - func shrink() { + + func shrink(completion:(() -> Void)?) { let shrinkAnimation = CABasicAnimation(keyPath: "bounds.size.width") shrinkAnimation.fromValue = frame.width shrinkAnimation.toValue = frame.height @@ -110,10 +133,16 @@ private extension UIButton { shrinkAnimation.timingFunction = shrinkCurve shrinkAnimation.fillMode = .forwards shrinkAnimation.isRemovedOnCompletion = false + + CATransaction.setCompletionBlock { + completion?() + } layer.add(shrinkAnimation, forKey: shrinkAnimation.keyPath) - + + CATransaction.commit() + } - + func animateToOriginalWidth(completion:(() -> Void)?) { let shrinkAnim = CABasicAnimation(keyPath: "bounds.size.width") shrinkAnim.fromValue = (self.bounds.height) @@ -122,15 +151,14 @@ private extension UIButton { shrinkAnim.timingFunction = shrinkCurve shrinkAnim.fillMode = .forwards shrinkAnim.isRemovedOnCompletion = false - + CATransaction.setCompletionBlock { -// self.setNeedsLayout() -// self.setNeedsDisplay() completion?() + self.isUserInteractionEnabled = true } self.layer.add(shrinkAnim, forKey: shrinkAnim.keyPath) - + CATransaction.commit() } - + } diff --git a/SpinningButton/Classes/Behaviors/EdgeLoadingBehavior.swift b/SpinningButton/Classes/Behaviors/EdgeLoadingBehavior.swift index 9261c88..6c420eb 100644 --- a/SpinningButton/Classes/Behaviors/EdgeLoadingBehavior.swift +++ b/SpinningButton/Classes/Behaviors/EdgeLoadingBehavior.swift @@ -9,13 +9,14 @@ import UIKit class EdgeLoadingBehavior: BaseBehavior { - + private var offset: CGFloat private var isTrailing = false - + private var titleWhileLoading: String? private var cachedTitle: String? - + + private var animationCompleted = false init(button: SpinningButton, offset: CGFloat = 4.0, isTrailing: Bool, titleWhileLoading: String? ) { self.offset = offset self.isTrailing = isTrailing @@ -23,20 +24,24 @@ class EdgeLoadingBehavior: BaseBehavior { super.init(button: button) setupConstraints() } - + override func startAnimating() { + animationCompleted = false guard let button = button else {return} UIView.animate(withDuration: 0.3, delay: 0.05, usingSpringWithDamping: 0.45, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { button.loadingSpinner.transform = CGAffineTransform(scaleX: 1, y: 1) }) { _ in button.loadingSpinner.startAnimating() } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.animationCompleted = true + } if let titleWhileLoading = titleWhileLoading { cachedTitle = button.title(for: .normal) button.setTitle(titleWhileLoading, for: .normal) } } - + private func setupConstraints() { guard let button = button else {return} button.addSubview(button.loadingSpinner) @@ -47,15 +52,21 @@ class EdgeLoadingBehavior: BaseBehavior { button.loadingSpinner.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: offset).isActive = true } } - + override func stopAnimating() { guard let button = button else {return} - button.loadingSpinner.stopAnimating() - - if titleWhileLoading != nil { - button.setTitle(cachedTitle, for: .normal) + + let delay = animationCompleted ? 0 : 1.0 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + + button.loadingSpinner.stopAnimating() + + if self.titleWhileLoading != nil { + button.setTitle(self.cachedTitle, for: .normal) + } + button.isUserInteractionEnabled = true } - + } - + }