diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 5284be8..4a0eba9 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ DCA9833A27D68AD400D6EA30 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9833927D68AD400D6EA30 /* ExampleApp.swift */; }; DCA9833E27D68AD600D6EA30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9833D27D68AD600D6EA30 /* Assets.xcassets */; }; DCA9834127D68AD600D6EA30 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */; }; - DCA9834B27D68AD600D6EA30 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9834A27D68AD600D6EA30 /* ExampleTests.swift */; }; DCA9836927D68E3500D6EA30 /* StoryboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836827D68E3500D6EA30 /* StoryboardView.swift */; }; DCA9836C27D6905000D6EA30 /* EntryPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836B27D6905000D6EA30 /* EntryPointView.swift */; }; DCA9837127D6912300D6EA30 /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */; }; @@ -26,6 +25,10 @@ DCA9838727D6A44700D6EA30 /* NavigationBarExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */; }; DCA9838927D6A55A00D6EA30 /* NavigationBarExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */; }; DCA9838B27D6A74800D6EA30 /* SwiftUIExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */; }; + F5F5B69C27D94B1800D998B5 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F5B69727D94B1800D998B5 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */; }; + F5F5B69D27D94B1800D998B5 /* GradientLoadingBarViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F5B69927D94B1800D998B5 /* GradientLoadingBarViewModelTestCase.swift */; }; + F5F5B69E27D94B1800D998B5 /* GradientActivityIndicatorViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F5B69A27D94B1800D998B5 /* GradientActivityIndicatorViewModelTestCase.swift */; }; + F5F5B69F27D94B1800D998B5 /* NotchGradientLoadingBarViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F5B69B27D94B1800D998B5 /* NotchGradientLoadingBarViewModelTestCase.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,7 +52,6 @@ DCA9833D27D68AD600D6EA30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DCA9834627D68AD600D6EA30 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DCA9834A27D68AD600D6EA30 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; DCA9836827D68E3500D6EA30 /* StoryboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardView.swift; sourceTree = ""; }; DCA9836B27D6905000D6EA30 /* EntryPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryPointView.swift; sourceTree = ""; }; DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = ""; }; @@ -63,6 +65,10 @@ DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarExampleViewController.swift; sourceTree = ""; }; DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NavigationBarExample.storyboard; sourceTree = ""; }; DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleView.swift; sourceTree = ""; }; + F5F5B69727D94B1800D998B5 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift"; sourceTree = ""; }; + F5F5B69927D94B1800D998B5 /* GradientLoadingBarViewModelTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientLoadingBarViewModelTestCase.swift; sourceTree = ""; }; + F5F5B69A27D94B1800D998B5 /* GradientActivityIndicatorViewModelTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientActivityIndicatorViewModelTestCase.swift; sourceTree = ""; }; + F5F5B69B27D94B1800D998B5 /* NotchGradientLoadingBarViewModelTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotchGradientLoadingBarViewModelTestCase.swift; sourceTree = ""; }; FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -149,7 +155,8 @@ DCA9834927D68AD600D6EA30 /* ExampleTests */ = { isa = PBXGroup; children = ( - DCA9834A27D68AD600D6EA30 /* ExampleTests.swift */, + F5F5B69827D94B1800D998B5 /* ViewModel */, + F5F5B69627D94B1800D998B5 /* Views */, ); path = ExampleTests; sourceTree = ""; @@ -229,6 +236,24 @@ path = NavigationBarExample; sourceTree = ""; }; + F5F5B69627D94B1800D998B5 /* Views */ = { + isa = PBXGroup; + children = ( + F5F5B69727D94B1800D998B5 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */, + ); + path = Views; + sourceTree = ""; + }; + F5F5B69827D94B1800D998B5 /* ViewModel */ = { + isa = PBXGroup; + children = ( + F5F5B69927D94B1800D998B5 /* GradientLoadingBarViewModelTestCase.swift */, + F5F5B69A27D94B1800D998B5 /* GradientActivityIndicatorViewModelTestCase.swift */, + F5F5B69B27D94B1800D998B5 /* NotchGradientLoadingBarViewModelTestCase.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -455,7 +480,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DCA9834B27D68AD600D6EA30 /* ExampleTests.swift in Sources */, + F5F5B69E27D94B1800D998B5 /* GradientActivityIndicatorViewModelTestCase.swift in Sources */, + F5F5B69C27D94B1800D998B5 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift in Sources */, + F5F5B69F27D94B1800D998B5 /* NotchGradientLoadingBarViewModelTestCase.swift in Sources */, + F5F5B69D27D94B1800D998B5 /* GradientLoadingBarViewModelTestCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -603,6 +631,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -633,6 +662,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -656,7 +686,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = F63GJC2AN7; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.ExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -677,7 +707,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = F63GJC2AN7; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.ExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Example/ExampleTests/ExampleTests.swift b/Example/ExampleTests/ExampleTests.swift deleted file mode 100644 index 643b940..0000000 --- a/Example/ExampleTests/ExampleTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ExampleTests.swift -// ExampleTests -// -// Created by Felix Mau on 07.03.22. -// - -import XCTest -@testable import Example - -class ExampleTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } -} diff --git a/Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift b/Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift new file mode 100644 index 0000000..4aafa95 --- /dev/null +++ b/Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift @@ -0,0 +1,243 @@ +// +// 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 initializer + + func test_initializer_shouldSetGradientLayerColors_toCorrectValue() { + let expectedGradientLayerColors = makeGradientLayerColors() + XCTAssertEqual(viewModel.gradientLayerColors.value, expectedGradientLayerColors) + } + + func test_initializer_shouldSetColorLocationMatrix_toCorrectValue() throws { + let receivedColorLocationMatrix = try XCTUnwrap(viewModel.colorLocationMatrix.value) + let expectedColorLocationMatrix = makeColorLocationMatrix() + + // Unfortunately there is no easier way comparing an array of type `NSNumber` / `Double` with a given accuracy. + XCTAssertEqual(receivedColorLocationMatrix.count, expectedColorLocationMatrix.count) + + for (receivedColorLocationRow, expectedColorLocationRow) in zip(receivedColorLocationMatrix, expectedColorLocationMatrix) { + XCTAssertEqual(receivedColorLocationRow.count, expectedColorLocationRow.count) + + for (receivedColorLocation, expectedColorLocation) in zip(receivedColorLocationRow, expectedColorLocationRow) { + XCTAssertEqual(receivedColorLocation.doubleValue, expectedColorLocation.doubleValue, accuracy: .ulpOfOne) + } + } + } + + func test_initializer_shouldSetAnimationDuration_toStaticConfigurationProperty() { + XCTAssertEqual(viewModel.animationDuration.value, TimeInterval.GradientLoadingBar.progressDuration) + } + + func test_initializer_shouldSetIsAnimating_toTrue() throws { + XCTAssertTrue( + try XCTUnwrap(viewModel.isAnimating.value) + ) + } + + func test_initializer_shouldSetGradientColors_toStaticConfigurationProperty() { + XCTAssertEqual(viewModel.gradientColors, UIColor.GradientLoadingBar.gradientColors) + } + + func test_initializer_shouldSetProgressAnimationDuration_toStaticConfigurationProperty() { + XCTAssertEqual(viewModel.progressAnimationDuration, TimeInterval.GradientLoadingBar.progressDuration) + } + + func test_initializer_ShouldSetIsHidden_toFalse() { + XCTAssertFalse(viewModel.isHidden) + } + + // MARK: - Test property `gradientColors` + + func test_settingGradientColors_shouldUpdateGradientLayerColorsObservable() { + // 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(\.cgColor) + + XCTAssertEqual(viewModel.gradientLayerColors.value, expectedGradientLayerColors) + } + + func test_settingGradientColors_shouldUpdateColorLocationMatrixObservable() { + // Given + let gradientColors: [UIColor] = [.red, .yellow, .green] + + // When + viewModel.gradientColors = gradientColors + + // Then + // + // `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 colorLocationMatrix = [ + [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], + ] + + let expectedColorLocationMatrix = colorLocationMatrix.map { + $0.map { NSNumber(value: $0) } + } + + XCTAssertEqual(viewModel.colorLocationMatrix.value, expectedColorLocationMatrix) + } + + // MARK: - Test property `progressAnimationDuration` + + func test_settingProgressAnimationDuration_shouldUpdateAnimationDurationObservable() { + // Given + let progressAnimationDuration: TimeInterval = 123 + + // When + viewModel.progressAnimationDuration = progressAnimationDuration + + // Then + XCTAssertEqual(viewModel.animationDuration.value, progressAnimationDuration) + } + + // MARK: - Test property `isHidden` + + func test_settingIsHiddenToTrue_shouldSetIsAnimatingToFalse() throws { + // When + viewModel.isHidden = true + + // Then + XCTAssertFalse( + try XCTUnwrap(viewModel.isAnimating.value) + ) + } + + func test_settingIsHiddenToFalse_shouldSetIsAnimatingToTrue() throws { + // When + viewModel.isHidden = false + + // Then + XCTAssertTrue( + try XCTUnwrap(viewModel.isAnimating.value) + ) + } +} + +// MARK: - Helpers + +extension GradientActivityIndicatorViewModelTestCase { + 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), + ] + + 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(\.cgColor) + } + + private func makeColorLocationInitialRow() -> ColorLocationRow { + 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 = Array(repeating: 0.0, count: 10) + gradientLocations + + return gradientLocationAnimationMatrixInitialRow.map { + NSNumber(value: $0) + } + } + + private func makeColorLocationMatrix() -> ColorLocationMatrix { + 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 + // ... + // 0 | 0 | 0 | 0 | 0 | 0 | 0.2 | 0.4 | 0.6 | 0.8 | 1 | 1 | 1 | 1 | 1 | 1 + // ... + // 0 | 0.2 | 0.4 | 0.6 | 0.8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 + // + let colorLocationMatrix = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0.2, 0.4, 0.6, 0.8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + ] + + return colorLocationMatrix.map { + $0.map { NSNumber(value: $0) } + } + } +} diff --git a/Example/ExampleTests/ViewModel/GradientLoadingBarViewModelTestCase.swift b/Example/ExampleTests/ViewModel/GradientLoadingBarViewModelTestCase.swift new file mode 100644 index 0000000..3f73e1f --- /dev/null +++ b/Example/ExampleTests/ViewModel/GradientLoadingBarViewModelTestCase.swift @@ -0,0 +1,125 @@ +// +// 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 = KeyWindow() + let passiveWindow = PassiveWindow() + sharedApplicationMock.windows = [keyWindow, passiveWindow] + + // When + let viewModel = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock, + notificationCenter: notificationCenter) + + // Then + XCTAssertEqual(viewModel.superview.value, keyWindow) + } + + func test_initializer_shouldSetupSuperviewObservable_afterUIWindowDidBecomeKeyNotification() { + // Given + let keyWindow = KeyWindow() + let passiveWindow = PassiveWindow() + sharedApplicationMock.windows = [passiveWindow] + + let viewModel = GradientLoadingBarViewModel(sharedApplication: sharedApplicationMock, + notificationCenter: notificationCenter) + + // When + sharedApplicationMock.windows.append(keyWindow) + notificationCenter.post(name: UIWindow.didBecomeKeyNotification, + object: nil) + + // Then + XCTAssertEqual(viewModel.superview.value, keyWindow) + } + + func test_deinit_shouldResetSuperviewObservable_withNil() { + // Given + let keyWindow = KeyWindow() + let passiveWindow = PassiveWindow() + sharedApplicationMock.windows = [keyWindow, passiveWindow] + + 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: - Helpers + +private final class KeyWindow: UIWindow { + override var isKeyWindow: Bool { true } +} + +private final class PassiveWindow: UIWindow { + override var isKeyWindow: Bool { false } +} + +// MARK: - Mocks + +private final class SharedApplicationMock: UIApplicationProtocol { + var windows = [UIWindow]() +} diff --git a/Example/ExampleTests/ViewModel/NotchGradientLoadingBarViewModelTestCase.swift b/Example/ExampleTests/ViewModel/NotchGradientLoadingBarViewModelTestCase.swift new file mode 100644 index 0000000..9be5c57 --- /dev/null +++ b/Example/ExampleTests/ViewModel/NotchGradientLoadingBarViewModelTestCase.swift @@ -0,0 +1,76 @@ +// +// 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", "iPhone11,8"] + deviceIdentifiers.forEach { deviceIdentifier in + + // When + let viewModel = NotchGradientLoadingBarViewModel(deviceIdentifier: deviceIdentifier) + + // Then + XCTAssertEqual(viewModel.safeAreaDevice, .iPhoneX) + } + } + + 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) + } +} diff --git a/Example/ExampleTests/Views/GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift b/Example/ExampleTests/Views/GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift new file mode 100644 index 0000000..30696c4 --- /dev/null +++ b/Example/ExampleTests/Views/GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift @@ -0,0 +1,207 @@ +// +// GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift +// ExampleTests +// +// Created by Felix Mau on 19.05.19. +// Copyright © 2017 Felix Mau. All rights reserved. +// + +import XCTest + +@testable import GradientLoadingBar + +// swiftlint:disable:next type_name +final class GradientActivityIndicatorViewAnimateIsHiddenTestCase: XCTestCase { + // MARK: - Private properties + + private var window: UIWindow! + private var gradientActivityIndicatorView: GradientActivityIndicatorView! + + // MARK: - Public methods + + override func setUp() { + super.setUp() + + // In order for UIView animations to be executed correctly, the corresponding view has to be attached to a visible window. + // Therefore we're gonna use the current key-window, add our testing view here in `setUp()` and remove it later in `tearDown()`. + window = UIApplication.shared.windows.first { $0.isKeyWindow } + + gradientActivityIndicatorView = GradientActivityIndicatorView() + window.addSubview(gradientActivityIndicatorView) + } + + override func tearDown() { + gradientActivityIndicatorView.removeFromSuperview() + gradientActivityIndicatorView = nil + + window = nil + + super.tearDown() + } + + // MARK: - Test method `animate(isHidden:)` + + func test_animateIsHidden_shouldShowView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // Hide view to validate fade-in. + gradientActivityIndicatorView.alpha = 0 + gradientActivityIndicatorView.isHidden = true + + // When + gradientActivityIndicatorView.animate(isHidden: false, duration: 0.1) { isFinished in + XCTAssertTrue(isFinished) + expectation.fulfill() + } + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden) + XCTAssertEqual(gradientActivityIndicatorView.alpha, 1.0, accuracy: CGFloat.ulpOfOne) + } + + func test_animateIsHidden_withInterruption_shouldShowView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // Hide view to validate fade-in. + gradientActivityIndicatorView.alpha = 0 + gradientActivityIndicatorView.isHidden = true + + // When + gradientActivityIndicatorView.animate(isHidden: false, duration: 0.1) { isFinished in + XCTAssertFalse(isFinished) + expectation.fulfill() + } + + // Cancel animation. + gradientActivityIndicatorView.layer.removeAllAnimations() + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden) + } + + func test_animateIsHidden_shouldHideView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // When + gradientActivityIndicatorView.animate(isHidden: true, duration: 0.1) { isFinished in + XCTAssertTrue(isFinished) + expectation.fulfill() + } + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertTrue(gradientActivityIndicatorView.isHidden) + XCTAssertEqual(gradientActivityIndicatorView.alpha, 0, accuracy: CGFloat.ulpOfOne) + } + + func test_animateIsHidden_withInterruption_shouldNotHideView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // When + gradientActivityIndicatorView.animate(isHidden: true, duration: 0.1) { isFinished in + XCTAssertFalse(isFinished) + expectation.fulfill() + } + + // Cancel animation. + gradientActivityIndicatorView.layer.removeAllAnimations() + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden, "As we've interrupted the animation, we expect the `isHidden` flag to still be `false`.") + } + + // MARK: - Test method `fadeIn()` + + func test_fadeIn_shouldShowView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // Hide view to validate fade-in. + gradientActivityIndicatorView.alpha = 0 + gradientActivityIndicatorView.isHidden = true + + // When + gradientActivityIndicatorView.fadeIn(duration: 0.1) { isFinished in + XCTAssertTrue(isFinished) + expectation.fulfill() + } + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden) + XCTAssertEqual(gradientActivityIndicatorView.alpha, 1.0, accuracy: CGFloat.ulpOfOne) + } + + func test_fadeIn_withInterruption_shouldShowView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // Hide view to validate fade-in. + gradientActivityIndicatorView.alpha = 0 + gradientActivityIndicatorView.isHidden = true + + // When + gradientActivityIndicatorView.fadeIn(duration: 0.1) { isFinished in + XCTAssertFalse(isFinished) + expectation.fulfill() + } + + // Cancel animation. + gradientActivityIndicatorView.layer.removeAllAnimations() + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden) + } + + // MARK: - Test method `fadeOut()` + + func test_fadeOut_shouldHideView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // When + gradientActivityIndicatorView.fadeOut(duration: 0.1) { isFinished in + XCTAssertTrue(isFinished) + expectation.fulfill() + } + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertTrue(gradientActivityIndicatorView.isHidden) + XCTAssertEqual(gradientActivityIndicatorView.alpha, 0, accuracy: CGFloat.ulpOfOne) + } + + func test_fadeOut_withInterruption_shouldNotHideView_andCallCompletionHandler() { + // Given + let expectation = expectation(description: "Expect completion handler to be called.") + + // When + gradientActivityIndicatorView.fadeOut(duration: 0.1) { isFinished in + XCTAssertFalse(isFinished) + expectation.fulfill() + } + + // Cancel animation. + gradientActivityIndicatorView.layer.removeAllAnimations() + + // Then + wait(for: [expectation], timeout: 1) + + XCTAssertFalse(gradientActivityIndicatorView.isHidden, "As we've interrupted the animation, we expect the `isHidden` flag to still be `false`.") + } +} diff --git a/Example/fastlane/Fastfile b/Example/fastlane/Fastfile index d42044b..e926062 100644 --- a/Example/fastlane/Fastfile +++ b/Example/fastlane/Fastfile @@ -18,10 +18,12 @@ default_platform(:ios) platform :ios do desc "Execute SwiftFormat and treat any formatting errors as real errors." lane :format do - swiftformat(executable: "Pods/SwiftFormat/CommandLineTool/swiftformat", - config: "Pods/SwiftConfigurationFiles/.swiftformat", - path: "../", - lint: true) + swiftformat( + executable: "Pods/SwiftFormat/CommandLineTool/swiftformat", + config: "Pods/SwiftConfigurationFiles/.swiftformat", + path: "../", + lint: true + ) end desc "Execute SwiftLint and treat any formatting errors as real errors." @@ -35,9 +37,11 @@ platform :ios do desc "Execute tests." lane :tests do - run_tests(workspace: "GradientLoadingBar.xcworkspace", - devices: ["iPhone 13"], - scheme: "GradientLoadingBar-Example") + run_tests( + workspace: "GradientLoadingBar.xcworkspace", + devices: ["iPhone 13"], + scheme: "GradientLoadingBar-Example" + ) end desc "Execute validation of library."