diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 07d83bd..6af2642 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 4B5A5EBA97A912894C10D040B5A1225D /* Pods-GradientLoadingBar_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = B8B5E60915249521CF9DC7208F2D5774 /* Pods-GradientLoadingBar_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 604C51928D4CFDAE15A6CAB6327B68D6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B802EDB707F8CE8EBD66EB815051159F /* Foundation.framework */; }; 640E8ABC2313DD280008C5A6 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640E8ABB2313DD280008C5A6 /* Constants.swift */; }; + 640E8ABE2313E4540008C5A6 /* GradientActivityIndicatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640E8ABD2313E4540008C5A6 /* GradientActivityIndicatorViewModel.swift */; }; 681130A53798D8143A2C972C3351D165 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B802EDB707F8CE8EBD66EB815051159F /* Foundation.framework */; }; 757352462D522BDB0B228DE9880106ED /* Pods-GradientLoadingBar_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E014E4CD6DE59FD4C7C3D12C2BC9474 /* Pods-GradientLoadingBar_Tests-dummy.m */; }; 7D00DA81B76090CC52ADBEC7E56F8A4B /* GradientLoadingBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D0266A0CC59CB31FD536223C0F4096 /* GradientLoadingBarController.swift */; }; @@ -125,6 +126,7 @@ 5459A35FCB502D3A65BE88C4A1A7C819 /* GradientActivityIndicatorView+AnimateIsHidden.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "GradientActivityIndicatorView+AnimateIsHidden.swift"; sourceTree = ""; }; 59286ACC8816C332C7919AEBF42BC6EB /* GradientLoadingBar.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; path = GradientLoadingBar.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 640E8ABB2313DD280008C5A6 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 640E8ABD2313E4540008C5A6 /* GradientActivityIndicatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientActivityIndicatorViewModel.swift; sourceTree = ""; }; 670D1598351BCDDA2ACA6F3ECB0434BB /* SwiftFormat.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftFormat.xcconfig; sourceTree = ""; }; 67873E92210DD4E777C49C6E8AEDC108 /* GradientLoadingBar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GradientLoadingBar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7471906D53E2B2BEDC58FEDF3DC82244 /* Pods-GradientLoadingBar_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-GradientLoadingBar_Example-dummy.m"; sourceTree = ""; }; @@ -302,6 +304,7 @@ isa = PBXGroup; children = ( 8AB9BDBFBE2396FB45EEB12E7B9C6067 /* GradientLoadingBarViewModel.swift */, + 640E8ABD2313E4540008C5A6 /* GradientActivityIndicatorViewModel.swift */, ); name = ViewModel; path = GradientLoadingBar/Classes/ViewModel; @@ -625,6 +628,7 @@ 8BD7971BD44E1BB5E96B9BD7F1047F0F /* GradientActivityIndicatorView+AnimateIsHidden.swift in Sources */, B4876567964EBC1B95C76559240D1AAB /* GradientActivityIndicatorView.swift in Sources */, 7F0493B920FABFACCE19457C63AD9719 /* GradientLoadingBar-dummy.m in Sources */, + 640E8ABE2313E4540008C5A6 /* GradientActivityIndicatorViewModel.swift in Sources */, 7D00DA81B76090CC52ADBEC7E56F8A4B /* GradientLoadingBarController.swift in Sources */, 3F7EF08A0DB32C7543CFDDB67068D548 /* GradientLoadingBarViewModel.swift in Sources */, 640E8ABC2313DD280008C5A6 /* Constants.swift in Sources */, diff --git a/GradientLoadingBar/Classes/ViewModel/GradientActivityIndicatorViewModel.swift b/GradientLoadingBar/Classes/ViewModel/GradientActivityIndicatorViewModel.swift new file mode 100644 index 0000000..bc70e14 --- /dev/null +++ b/GradientLoadingBar/Classes/ViewModel/GradientActivityIndicatorViewModel.swift @@ -0,0 +1,73 @@ +// +// GradientActivityIndicatorViewModel.swift +// GradientLoadingBar +// +// Created by Felix Mau on 08/26/19. +// Copyright © 2019 Felix Mau. All rights reserved. +// + +import Foundation +import LightweightObservable + +class GradientActivityIndicatorViewModel { + // MARK: - Types + + /// + enum AnimationState: Equatable { + case none + case animating(duration: TimeInterval) + } + + // MARK: - Public properties + + /// + var animationState: Observable { + return animationStateSubject.asObservable + } + + /// + var infinteGradientColors: Observable<[CGColor]> { + return infinteGradientColorsSubject.asObservable + } + + /// + var isHidden = false { + didSet { + if isHidden { + animationStateSubject.value = .none + } else { + animationStateSubject.value = .animating(duration: progressAnimationDuration) + } + } + } + + /// Colors used for the gradient. + var gradientColors = UIColor.GradientLoadingBar.gradientColors { + didSet { + infinteGradientColorsSubject.value = makeInfiniteGradientColors() + } + } + + /// Duration for the progress animation. + var progressAnimationDuration = TimeInterval.GradientLoadingBar.progressDuration + + // MARK: - Private properties + + private let infinteGradientColorsSubject: Variable<[CGColor]> = Variable([]) + + private let animationStateSubject: Variable = Variable(.none) + + // MARK: - Private methods + + /// Simulate infinte animation - Therefore we'll reverse the colors and remove the first and last item + /// to prevent duplicate values at the "inner edges" destroying the infinite look. + private func makeInfiniteGradientColors() -> [CGColor] { + let reversedColors = gradientColors + .reversed() + .dropFirst() + .dropLast() + + let infiniteGradientColors = gradientColors + reversedColors + gradientColors + return infiniteGradientColors.map { $0.cgColor } + } +} diff --git a/GradientLoadingBar/Classes/Views/GradientActivityIndicatorView.swift b/GradientLoadingBar/Classes/Views/GradientActivityIndicatorView.swift index e4ab87b..434708d 100644 --- a/GradientLoadingBar/Classes/Views/GradientActivityIndicatorView.swift +++ b/GradientLoadingBar/Classes/Views/GradientActivityIndicatorView.swift @@ -7,6 +7,7 @@ // import UIKit +import LightweightObservable @IBDesignable open class GradientActivityIndicatorView: UIView { @@ -19,37 +20,32 @@ open class GradientActivityIndicatorView: UIView { open override var isHidden: Bool { didSet { - if isHidden { - stopProgressAnimation() - } else { - startProgressAnimation() - } + viewModel.isHidden = isHidden } } /// Duration for the progress animation. - public var progressAnimationDuration = TimeInterval.GradientLoadingBar.progressDuration + public var progressAnimationDuration: TimeInterval { + get { + return viewModel.progressAnimationDuration + } + set { + viewModel.progressAnimationDuration = newValue + } + } /// Colors used for the gradient. - public var gradientColorList = UIColor.GradientLoadingBar.gradientColorList { - didSet { - gradientLayer.colors = infinteColorList.map { $0.cgColor } + public var gradientColors: [UIColor] { + get { + return viewModel.gradientColors + } + set { + viewModel.gradientColors = newValue } } // MARK: - Private properties - /// Simulate infinte animation - Therefore we'll reverse the colors and remove the first and last item - /// to prevent duplicate values at the "inner edges" destroying the infinite look. - private var infinteColorList: [UIColor] { - let reversedColorList = gradientColorList - .reversed() - .dropFirst() - .dropLast() - - return gradientColorList + reversedColorList + gradientColorList - } - /// Layer holding the gradient. private var gradientLayer: CAGradientLayer = { let layer = CAGradientLayer() @@ -61,6 +57,12 @@ open class GradientActivityIndicatorView: UIView { return layer }() + /// + private let viewModel = GradientActivityIndicatorViewModel() + + /// The dispose bag for the observables. + private var disposeBag = DisposeBag() + // MARK: - Initializers public override init(frame: CGRect) { @@ -102,21 +104,35 @@ open class GradientActivityIndicatorView: UIView { // MARK: - Private methods private func commonInit() { - gradientLayer.colors = infinteColorList.map { $0.cgColor } layer.insertSublayer(gradientLayer, at: 0) layer.masksToBounds = true - startProgressAnimation() + bindViewModelToView() } - private func startProgressAnimation() { + private func bindViewModelToView() { + viewModel.infinteGradientColors.subscribeDistinct { [weak self] newInfinteGradientColors, _ in + self?.gradientLayer.colors = newInfinteGradientColors + }.disposed(by: &disposeBag) + + viewModel.animationState.subscribeDistinct { [weak self] newAnimationState, _ in + switch newAnimationState { + case .none: + self?.stopProgressAnimation() + case let .animating(duration): + self?.startProgressAnimation(duration: duration) + } + }.disposed(by: &disposeBag) + } + + private func startProgressAnimation(duration: TimeInterval) { let animation = CABasicAnimation(keyPath: "position") animation.fromValue = CGPoint(x: -2 * bounds.size.width, y: 0) animation.toValue = CGPoint.zero animation.isRemovedOnCompletion = false animation.repeatCount = Float.infinity - animation.duration = progressAnimationDuration + animation.duration = duration gradientLayer.add(animation, forKey: GradientActivityIndicatorView.progressAnimationKey) }