♻️ :: Animate locations property instead of entire layer

This commit is contained in:
Felix Mau
2019-10-11 17:07:08 +02:00
parent fb5def6a50
commit 8a3e71b879
3 changed files with 322 additions and 259 deletions
@@ -7,8 +7,8 @@
//
import XCTest
import LightweightObservable
@testable import LightweightObservable
@testable import GradientLoadingBar
// swiftlint:disable:next type_name
@@ -38,35 +38,21 @@ class GradientActivityIndicatorViewModelTestCase: XCTestCase {
// MARK: - Test initializer
func testInitializerShouldSetIsAnimatingProgressToTrue() {
XCTAssertTrue(viewModel.isAnimatingProgress.value)
func testInitializerShouldSetGradientLayerColorsToCorrectValue() {
let expectedGradientLayerColors = makeGradientLayerColors()
XCTAssertEqual(viewModel.gradientLayerColors.value, expectedGradientLayerColors)
}
func testInitializerShouldSetGradientLayerFrameToZero() {
XCTAssertEqual(viewModel.gradientLayerFrame.value, .zero)
}
func testInitializerShouldSetGradientLayerLocationsToCorrectValue() {
let receivedGradientLayerLocations = viewModel.gradientLayerLocations.value
let expectedGradientLayerLocations = makeGradientLocationAnimationMatrixInitialRow()
func testInitializerShouldSetGradientLayerAnimationFromValueToZero() {
XCTAssertEqual(viewModel.gradientLayerAnimationFromValue.value, 0.0)
}
// Unfortunately there is no easier way comparing an array of type `NSNumber` / `Double` with a given accuracy.
XCTAssertEqual(receivedGradientLayerLocations.count, expectedGradientLayerLocations.count)
func testInitializerShouldSetGradientLayerAnimationDurationToValueFromProgressAnimationDuration() {
XCTAssertEqual(viewModel.gradientLayerAnimationDuration.value, viewModel.progressAnimationDuration)
}
func testInitializerShouldSetGradientLayerColorsBasedOnCurrentGradientColors() {
let extectedGradientLayerColors =
makeGradientLayerColors(from: viewModel.gradientColors)
XCTAssertEqual(viewModel.gradientLayerColors.value, extectedGradientLayerColors)
}
func testInitializerShouldSetIsHiddenToFalse() {
XCTAssertFalse(viewModel.isHidden)
}
func testInitializerShouldSetBoundsToZero() {
XCTAssertEqual(viewModel.bounds, .zero)
for (receivedLocation, expectedLocation) in zip(receivedGradientLayerLocations, expectedGradientLayerLocations) {
XCTAssertEqual(receivedLocation.doubleValue, expectedLocation.doubleValue, accuracy: Double.ulpOfOne)
}
}
func testInitializerShouldSetGradientColorsToStaticConfigurationProperty() {
@@ -77,143 +63,173 @@ class GradientActivityIndicatorViewModelTestCase: XCTestCase {
XCTAssertEqual(viewModel.progressAnimationDuration, TimeInterval.GradientLoadingBar.progressDuration)
}
// MARK: - Test setting property `isHidden`
func testInitializerShouldSetIsHiddenToFalse() {
XCTAssertFalse(viewModel.isHidden)
}
func testSettingIsHiddenToTrueShouldUpdateIsAnimatingProgressToFalse() {
// MARK: - Test property `gradientColors`
func testSettingGradientColorsShouldUpdateGradientLayerColorsObservable() {
// Given
let gradientColors: [UIColor] = [.red, .yellow, .green]
// When
viewModel.gradientColors = gradientColors
// Then
//
// `gradientColors = [.red, .yellow, .green]`
// `gradientLayerColors = [.red, .yellow, .green, .yellow, .red, .yellow, .green]`
//
let expectedGradientColors: [UIColor] = [.red, .yellow, .green, .yellow, .red, .yellow, .green]
let expectedGradientLayerColors = expectedGradientColors.map { $0.cgColor }
XCTAssertEqual(viewModel.gradientLayerColors.value, expectedGradientLayerColors)
}
func testSettingGradientColorsShouldUpdateGradientLayerLocationsObservable() {
// Given
let gradientColors: [UIColor] = [.red, .yellow, .green]
// When
viewModel.gradientColors = gradientColors
// Then
//
// `gradientColors = [.red, .yellow, .green]`
// `gradientLayerColors = [.red, .yellow, .green, .yellow, .red, .yellow, .green]`
//
// gradientLayerColors | .red | .yellow | .green | .green | .yellow | .red | .yellow | .green
// initialLocations | 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 1
//
let expectedGradientLocations = [0, 0, 0, 0, 0, 0.5, 1]
let expectedGradientLayerLocations = expectedGradientLocations.map { NSNumber(value: $0) }
XCTAssertEqual(viewModel.gradientLayerLocations.value, expectedGradientLayerLocations)
}
// MARK: - Test property `isHidden`
func testSettingIsHiddenToTrueShouldInformDelegateToStopAnimatingLocations() {
// When
viewModel.isHidden = true
// Then
XCTAssertFalse(viewModel.isAnimatingProgress.value)
XCTAssertEqual(delegateMock.invokedMethod, .stopAnimatingLocations)
}
func testSettingIsHiddenToFalseShouldUpdateIsAnimatingProgressToTrue() {
func testSettingIsHiddenToFalseShouldInformDelegateToStartAnimatingLocations() {
// Given
// In order to simplify the matrix, we're gonna update the `gradientColors` first.
let gradientColors: [UIColor] = [.red, .yellow, .green]
viewModel.gradientColors = gradientColors
XCTAssertNil(delegateMock.invokedMethod,
"Precondition failed Expected delegate not to be informed at this point!")
// When
viewModel.isHidden = false
// Then
XCTAssertTrue(viewModel.isAnimatingProgress.value)
}
//
// `gradientColors = [.red, .yellow, .green]`
// `gradientLayerColors = [.red, .yellow, .green, .yellow, .red, .yellow, .green]`
//
// i | .red | .yellow | .green | .yellow | .red | .yellow | .green
// 0 | 0 | 0 | 0 | 0 | 0 | 0.5 | 1
// 1 | 0 | 0 | 0 | 0 | 0.5 | 1 | 1
// 2 | 0 | 0 | 0 | 0.5 | 1 | 1 | 1
// 3 | 0 | 0 | 0.5 | 1 | 1 | 1 | 1
// 4 | 0 | 0.5 | 1 | 1 | 1 | 1 | 1
//
let gradientLocationsMatrix = [
[0, 0, 0, 0, 0, 0.5, 1],
[0, 0, 0, 0, 0.5, 1, 1],
[0, 0, 0, 0.5, 1, 1, 1],
[0, 0, 0.5, 1, 1, 1, 1],
[0, 0.5, 1, 1, 1, 1, 1]
]
// MARK: - Test setting property `bounds`
let expectedValues = gradientLocationsMatrix.map {
$0.map { NSNumber(value: $0) }
}
func testSettingBoundsShouldUpdateGradientLayerFrame() {
// Given
let bounds = CGRect(x: 1.0, y: 2.0, width: 3.0, height: 4.0)
// When
viewModel.bounds = bounds
// Then
let expectedFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width * 3, height: bounds.height)
XCTAssertEqual(viewModel.gradientLayerFrame.value, expectedFrame)
}
func testSettingBoundsShouldUpdateGradientLayerAnimationFromValue() {
// Given
let bounds = CGRect(x: 1.0, y: 2.0, width: 3.0, height: 4.0)
// When
viewModel.bounds = bounds
// Then
let expectedAnimationFromValue = -2 * bounds.size.width
XCTAssertEqual(viewModel.gradientLayerAnimationFromValue.value, expectedAnimationFromValue)
}
func testSettingBoundsShouldInformDelegateToRestartAnimation() {
// When
viewModel.bounds = CGRect(x: 1.0, y: 2.0, width: 3.0, height: 4.0)
// Then
XCTAssertTrue(delegateMock.didCallRestartAnimation)
}
func testSettingBoundsShouldNotInformDelegateToRestartAnimationAsTheViewIsCurrentlyHidden() {
// Given
viewModel.isHidden = true
// When
viewModel.bounds = CGRect(x: 1.0, y: 2.0, width: 3.0, height: 4.0)
// Then
XCTAssertFalse(delegateMock.didCallRestartAnimation)
}
// MARK: - Test setting property `gradientColors`
func testSettingGradientColorsShouldUpdateGradientLayerColors() {
// Given
let colors: [UIColor] = [.red, .yellow, .green]
// When
viewModel.gradientColors = colors
// Then
let extectedGradientLayerColors =
makeGradientLayerColors(from: colors)
XCTAssertEqual(viewModel.gradientLayerColors.value, extectedGradientLayerColors)
}
// MARK: - Test setting property `progressAnimationDuration`
func testSettingProgressAnimationDurationShouldUpdateGradientLayerAnimationDuration() {
// Given
let progressAnimationDuration = 1.23
// When
viewModel.progressAnimationDuration = progressAnimationDuration
// Then
XCTAssertEqual(viewModel.gradientLayerAnimationDuration.value, progressAnimationDuration)
}
func testSettingProgressAnimationDurationShouldInformDelegateToRestartAnimation() {
// When
viewModel.progressAnimationDuration = 1.23
// Then
XCTAssertTrue(delegateMock.didCallRestartAnimation)
}
func testSettingProgressAnimationDurationShouldNotInformDelegateToRestartAnimationAsTheViewIsCurrentlyHidden() {
// Given
viewModel.isHidden = true
// When
viewModel.progressAnimationDuration = 1.23
// Then
XCTAssertFalse(delegateMock.didCallRestartAnimation)
XCTAssertEqual(delegateMock.invokedMethod, .startAnimatingLocations(values: expectedValues,
duration: viewModel.progressAnimationDuration))
}
}
// MARK: - Helpers
extension GradientActivityIndicatorViewModelTestCase {
private func makeGradientLayerColors(from gradientColors: [UIColor]) -> [CGColor] {
let reversedColors = gradientColors
.reversed()
.dropFirst()
.dropLast()
private func makeGradientLayerColors() -> [CGColor] {
let gradientColors = [
#colorLiteral(red: 0.2980392157, green: 0.8509803922, blue: 0.3921568627, alpha: 1), #colorLiteral(red: 0.3529411765, green: 0.7843137255, blue: 0.9803921569, alpha: 1), #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1), #colorLiteral(red: 0.2039215686, green: 0.6666666667, blue: 0.862745098, alpha: 1), #colorLiteral(red: 0.3450980392, green: 0.337254902, blue: 0.8392156863, alpha: 1), #colorLiteral(red: 1, green: 0.1764705882, blue: 0.3333333333, alpha: 1)
]
let infiniteGradientColors = gradientColors + reversedColors + gradientColors
XCTAssertEqual(gradientColors, UIColor.GradientLoadingBar.gradientColors,
"Precondition failed The given gradient colors do not match the current color constant!")
// swiftlint:disable:next identifier_name
let reversedGradientColorsWithoutFirstAndLastValue = [
#colorLiteral(red: 0.3450980392, green: 0.337254902, blue: 0.8392156863, alpha: 1), #colorLiteral(red: 0.2039215686, green: 0.6666666667, blue: 0.862745098, alpha: 1), #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1), #colorLiteral(red: 0.3529411765, green: 0.7843137255, blue: 0.9803921569, alpha: 1)
]
let infiniteGradientColors = gradientColors + reversedGradientColorsWithoutFirstAndLastValue + gradientColors
return infiniteGradientColors.map { $0.cgColor }
}
private func makeGradientLocations() -> [NSNumber] {
let gradientLocations = [0, 0.2, 0.4, 0.6, 0.8, 1]
XCTAssertEqual(gradientLocations.count, UIColor.GradientLoadingBar.gradientColors.count,
"Precondition failed The given gradient locations do not match for the current color constant!")
return gradientLocations.map { NSNumber(value: $0) }
}
private func makeGradientLocationAnimationMatrixInitialRow() -> [NSNumber] {
let gradientLocations = [0, 0.2, 0.4, 0.6, 0.8, 1]
XCTAssertEqual(gradientLocations.count, UIColor.GradientLoadingBar.gradientColors.count,
"Precondition failed The given gradient locations do not match for the current color constant!")
// The current constant has 6 value and therefore we expect 16 gradient layer colors in total.
// Ergo we have to fill the first 10 values with "0", before adding the `gradientLocations`.
//
// .green | .malibu | .azure | .curious | .violet | .red | .violet | .curious | .azure | .malibu | .green | .malibu | .azure | .curious | .violet | .red
// 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.2 | 0.4 | 0.6 | 0.8 | 1
//
// swiftlint:disable:next identifier_name
let gradientLocationAnimationMatrixInitialRow = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + gradientLocations
return gradientLocationAnimationMatrixInitialRow.map { NSNumber(value: $0) }
}
}
// MARK: - Mocks
// swiftlint:disable:next type_name
class GradientActivityIndicatorViewModelDelegateMock: GradientActivityIndicatorViewModelDelegate {
private class GradientActivityIndicatorViewModelDelegateMock: GradientActivityIndicatorViewModelDelegate {
// MARK: - Types
enum DelegateMethod: Equatable {
case startAnimatingLocations(values: GradientLocationAnimationMatrix, duration: TimeInterval)
case stopAnimatingLocations
}
// MARK: - Public properties
private(set) var didCallRestartAnimation = false
private(set) var invokedMethod: DelegateMethod?
// MARK: - Public methods
func restartAnimation() {
didCallRestartAnimation = true
func startAnimatingLocations(values: GradientLocationAnimationMatrix, duration: TimeInterval) {
invokedMethod = .startAnimatingLocations(values: values, duration: duration)
}
func stopAnimatingLocations() {
invokedMethod = .stopAnimatingLocations
}
}