Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant

# Conflicts:
#	Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift
#	GradientLoadingBar/Feature/GradientActivityIndicatorView/GradientActivityIndicatorViewModel.swift
This commit is contained in:
Felix Mau
2022-04-27 18:42:50 +02:00
148 changed files with 4085 additions and 4006 deletions
@@ -0,0 +1,213 @@
//
// GradientActivityIndicatorViewModelTestCase.swift
// ExampleTests
//
// Created by Felix Mau on 26.08.19.
// Copyright © 2019 Felix Mau. All rights reserved.
//
import XCTest
import LightweightObservable
@testable import GradientLoadingBar
// swiftlint:disable:next type_name
final class GradientActivityIndicatorViewModelTestCase: XCTestCase {
// MARK: - Private properties
private var viewModel: GradientActivityIndicatorViewModel!
// MARK: - Public methods
override func setUp() {
super.setUp()
viewModel = GradientActivityIndicatorViewModel()
}
override func tearDown() {
viewModel = nil
super.tearDown()
}
// MARK: - Test property `isHidden`
func test_settingIsHidden_toTrue_shouldUpdateIsAnimatingSubject_withFalse() throws {
// Given
var receivedIsAnimating: Bool?
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating = isAnimating
}
let isHidden = true
// When
withExtendedLifetime(disposable) {
viewModel.isHidden = isHidden
}
// Then
let expectedIsAnimating = !isHidden
XCTAssertEqual(receivedIsAnimating, expectedIsAnimating)
}
func test_settingIsHidden_toFalse_shouldUpdateIsAnimatingSubject_withTrue() throws {
// Given
var receivedIsAnimating: Bool?
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating = isAnimating
}
let isHidden = false
// When
withExtendedLifetime(disposable) {
viewModel.isHidden = isHidden
}
// Then
let expectedIsAnimating = !isHidden
XCTAssertEqual(receivedIsAnimating, expectedIsAnimating)
}
// MARK: - Test property `bounds`
func test_settingBounds_shouldUpdateGradientLayerSizeUpdate() {
// Given
var receivedSizeUpdate: GradientActivityIndicatorViewModel.SizeUpdate?
let disposable = viewModel.gradientLayerSizeUpdate.subscribe { sizeUpdate, _ in
receivedSizeUpdate = sizeUpdate
}
let size = CGSize(width: .random(in: 1 ... 100), height: .random(in: 1 ... 100))
let bounds = CGRect(origin: .zero, size: size)
// When
withExtendedLifetime(disposable) {
viewModel.bounds = bounds
}
// Then
let expectedSizeUpdate =
GradientActivityIndicatorViewModel.SizeUpdate(frame: CGRect(x: 0, y: 0, width: size.width * 3, height: size.height),
fromValue: size.width * -2)
XCTAssertEqual(receivedSizeUpdate, expectedSizeUpdate)
}
func test_settingBounds_shouldRestartAnimation() {
// Given
var receivedIsAnimating = [Bool]()
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating.append(isAnimating)
}
receivedIsAnimating.removeAll()
// When
withExtendedLifetime(disposable) {
viewModel.bounds = CGRect(origin: .zero, size: .zero)
}
// Then
let expectedIsAnimating = [false, true]
XCTAssertEqual(receivedIsAnimating, expectedIsAnimating)
}
func test_settingBounds_shouldNotRestartAnimation_dueToViewIsHidden() {
// Given
var receivedIsAnimating = [Bool]()
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating.append(isAnimating)
}
viewModel.isHidden = true
receivedIsAnimating.removeAll()
// When
withExtendedLifetime(disposable) {
viewModel.bounds = CGRect(origin: .zero, size: .zero)
}
// Then
XCTAssertTrue(receivedIsAnimating.isEmpty)
}
// MARK: - Test property `gradientColors`
func test_settingGradientColors_shouldUpdateGradientLayerColors() {
// Given
var receivedGradientLayerColors: [CGColor]?
let disposable = viewModel.gradientLayerColors.subscribe { gradientLayerColors, _ in
receivedGradientLayerColors = gradientLayerColors
}
let gradientColors: [UIColor] = [.red, .yellow, .green]
// When
withExtendedLifetime(disposable) {
viewModel.gradientColors = gradientColors
}
// Then
let expectedGradientLayerColors = [UIColor.red, .yellow, .green, .yellow, .red, .yellow, .green].map { $0.cgColor }
XCTAssertEqual(receivedGradientLayerColors, expectedGradientLayerColors)
}
// MARK: - Test property `progressAnimationDuration`
func test_settingProgressAnimationDuration_shouldUpdategGradientLayerAnimationDuration() {
// Given
var receivedGradientLayerAnimationDuration: TimeInterval?
let disposable = viewModel.gradientLayerAnimationDuration.subscribe { gradientLayerAnimationDuration, _ in
receivedGradientLayerAnimationDuration = gradientLayerAnimationDuration
}
let progressAnimationDuration: TimeInterval = .random(in: 0 ... 100)
// When
withExtendedLifetime(disposable) {
viewModel.progressAnimationDuration = progressAnimationDuration
}
// Then
XCTAssertEqual(receivedGradientLayerAnimationDuration, progressAnimationDuration)
}
func test_settingProgressAnimationDuration_shouldRestartAnimation() {
// Given
var receivedIsAnimating = [Bool]()
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating.append(isAnimating)
}
receivedIsAnimating.removeAll()
// When
withExtendedLifetime(disposable) {
viewModel.progressAnimationDuration = .random(in: 0 ... 100)
}
// Then
let expectedIsAnimating = [false, true]
XCTAssertEqual(receivedIsAnimating, expectedIsAnimating)
}
func test_settingProgressAnimationDuration_shouldNotRestartAnimation_dueToViewIsHidden() {
// Given
var receivedIsAnimating = [Bool]()
let disposable = viewModel.isAnimating.subscribe { isAnimating, _ in
receivedIsAnimating.append(isAnimating)
}
viewModel.isHidden = true
receivedIsAnimating.removeAll()
// When
withExtendedLifetime(disposable) {
viewModel.progressAnimationDuration = .random(in: 0 ... 100)
}
// Then
XCTAssertTrue(receivedIsAnimating.isEmpty)
}
}
@@ -0,0 +1,81 @@
//
// GradientLoadingBarView+ViewModelTestCase.swift
// ExampleTests
//
// Created by Felix Mau on 22.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import XCTest
import SwiftUI
@testable import GradientLoadingBar
// swiftlint:disable:next type_name
final class GradientLoadingBarView_ViewModelTestCase: XCTestCase {
// MARK: - Test property `gradientColors`
func test_initialGradientColors_shouldIncludeReversedGradientColors() {
// Given
let gradientColors: [Color] = [.red, .yellow, .green]
// When
let viewModel = GradientLoadingBarView.ViewModel(gradientColors: gradientColors, progressDuration: 1)
// Then
let expectedGradientColors: [Color] = [.red, .yellow, .green, .yellow, .red, .yellow, .green]
XCTAssertEqual(viewModel.gradientColors, expectedGradientColors)
}
// MARK: - Test property `horizontalOffset`
func test_initialHorizontalOffset_shouldBeZero() {
// When
let viewModel = GradientLoadingBarView.ViewModel(gradientColors: [], progressDuration: 1)
// Then
XCTAssertEqual(viewModel.horizontalOffset, 0)
}
func test_settingSize_shouldUpdateHorizontalOffset() {
// Given
let viewModel = GradientLoadingBarView.ViewModel(gradientColors: [], progressDuration: 1)
let size = CGSize(width: .random(in: 1 ... 100_000), height: .random(in: 1 ... 100_000))
var receivedValues = [CGFloat]()
let subscription = viewModel.$horizontalOffset.sink {
receivedValues.append($0)
}
withExtendedLifetime(subscription) {
// When
viewModel.size = size
}
// Then
let expectedValues = [0, -size.width, size.width]
XCTAssertEqual(receivedValues, expectedValues)
}
// MARK: - Test property `gradientWidth`
func test_initialGradientWidth_shouldBeZero() {
// When
let viewModel = GradientLoadingBarView.ViewModel(gradientColors: [], progressDuration: 1)
// Then
XCTAssertEqual(viewModel.gradientWidth, 0)
}
func test_settingSize_shouldUpdateGradientWidth() {
// Given
let viewModel = GradientLoadingBarView.ViewModel(gradientColors: [], progressDuration: 1)
let size = CGSize(width: .random(in: 1 ... 100_000), height: .random(in: 1 ... 100_000))
// When
viewModel.size = size
// Then
XCTAssertEqual(viewModel.gradientWidth, size.width * 3)
}
}
@@ -0,0 +1,113 @@
//
// GradientLoadingBarViewModelTestCase.swift
// ExampleTests
//
// Created by Felix Mau on 26.12.17.
// Copyright © 2017 Felix Mau. All rights reserved.
//
import XCTest
import LightweightObservable
@testable import GradientLoadingBar
final class GradientLoadingBarViewModelTestCase: XCTestCase {
// MARK: - Private properties
private var sharedApplicationMock: SharedApplicationMock!
private var notificationCenter: NotificationCenter!
// MARK: - Public methods
override func setUp() {
super.setUp()
sharedApplicationMock = SharedApplicationMock()
notificationCenter = NotificationCenter()
}
override func tearDown() {
notificationCenter = nil
sharedApplicationMock = nil
super.tearDown()
}
// MARK: - Test observable `superview`
func test_initializer_shouldSetupSuperviewObservable_withNil() throws {
// When
let viewModel = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock,
notificationCenter: notificationCenter)
// Then
let variable = try XCTUnwrap(viewModel.superview as? Variable, "Cast `Observable` instance to `Variable` in order to validate the initial value.")
XCTAssertNil(variable.value)
}
func test_initializer_shouldSetupSuperviewObservable_withKeyWindow() {
// Given
let keyWindow = UIWindow()
sharedApplicationMock.keyWindowInConnectedScenes = keyWindow
// When
let viewModel = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock,
notificationCenter: notificationCenter)
// Then
XCTAssertEqual(viewModel.superview.value, keyWindow)
}
func test_initializer_shouldSetupSuperviewObservable_afterUIWindowDidBecomeKeyNotification() {
// Given
sharedApplicationMock.keyWindowInConnectedScenes = nil
let viewModel = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock,
notificationCenter: notificationCenter)
// When
let keyWindow = UIWindow()
sharedApplicationMock.keyWindowInConnectedScenes = keyWindow
notificationCenter.post(name: UIWindow.didBecomeKeyNotification,
object: nil)
// Then
XCTAssertEqual(viewModel.superview.value, keyWindow)
}
func test_deinit_shouldResetSuperviewObservable_withNil() {
// Given
let keyWindow = UIWindow()
sharedApplicationMock.keyWindowInConnectedScenes = keyWindow
var viewModel: GradientLoadingBarViewModel? = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock,
notificationCenter: notificationCenter)
let expectation = expectation(description: "Expected observer to be informed to reset superview to nil.")
var disposeBag = DisposeBag()
// As we've just initialized the view model it has to exist at this point, and therefore we can "safely" use force-unwrapping here.
// swiftlint:disable:next force_unwrapping
viewModel!.superview.subscribe { newSuperview, _ in
guard newSuperview == nil else {
// Skip initial call to observer.
return
}
expectation.fulfill()
}.disposed(by: &disposeBag)
// When
viewModel = nil
// Then
wait(for: [expectation], timeout: 0.1)
}
}
// MARK: - Mocks
private final class SharedApplicationMock: UIApplicationProtocol {
var keyWindowInConnectedScenes: UIWindow?
}
@@ -0,0 +1,89 @@
//
// NotchGradientLoadingBarViewModelTestCase.swift
// ExampleTests
//
// Created by Felix Mau on 26.12.17.
// Copyright © 2017 Felix Mau. All rights reserved.
//
import XCTest
@testable import GradientLoadingBar
final class NotchGradientLoadingBarViewModelTestCase: XCTestCase {
func test_initializer_shouldSetSafeAreaDevice_toIPhoneX() {
// Given
let deviceIdentifiers = ["iPhone10,3", "iPhone10,6", "iPhone11,2", "iPhone11,4", "iPhone11,6"]
deviceIdentifiers.forEach { deviceIdentifier in
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .iPhoneX)
}
}
func test_initializer_shouldSetSafeAreaDevice_toIPhoneXR() {
// Given
let deviceIdentifiers = ["iPhone11,8"]
deviceIdentifiers.forEach { deviceIdentifier in
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .iPhoneXR)
}
}
func test_initializer_shouldSetSafeAreaDevice_toIPhone11() {
// Given
let deviceIdentifiers = ["iPhone12,1", "iPhone12,3", "iPhone12,5"]
deviceIdentifiers.forEach { deviceIdentifier in
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .iPhone11)
}
}
func test_initializer_shouldSetSafeAreaDevice_toIPhone12() {
// Given
let deviceIdentifiers = ["iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4"]
deviceIdentifiers.forEach { deviceIdentifier in
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .iPhone12)
}
}
func test_initializer_shouldSetSafeAreaDevice_toIPhone13() {
// Given
let deviceIdentifiers = ["iPhone14,4", "iPhone14,5", "iPhone14,2", "iPhone14,3"]
deviceIdentifiers.forEach { deviceIdentifier in
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .iPhone13)
}
}
func test_initializer_shouldSetSafeAreaDevice_toUnknown() {
// Given
let deviceIdentifier = "Foo-Bar-🤡"
// When
let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier)
// Then
XCTAssertEqual(viewModel.safeAreaDevice, .unknown)
}
}