mirror of
https://github.com/fxm90/GradientLoadingBar.git
synced 2026-06-16 12:24:31 +00:00
Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant
This commit is contained in:
-7
@@ -30,14 +30,7 @@ final class GradientLoadingBarControllerTestCase: XCTestCase {
|
||||
// When
|
||||
gradientLoadingBarController.fadeIn(duration: 0)
|
||||
|
||||
let expectation = expectation(description: "Wait for view to appear.")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
// Then
|
||||
wait(for: [expectation], timeout: 1)
|
||||
|
||||
assertSnapshot(matching: rootViewController,
|
||||
as: .image(drawHierarchyInKeyWindow: true, precision: Config.precision))
|
||||
}
|
||||
|
||||
-7
@@ -31,14 +31,7 @@ final class NotchGradientLoadingBarControllerTestCase: XCTestCase {
|
||||
// When
|
||||
notchGradientLoadingBarController.fadeIn(duration: 0)
|
||||
|
||||
let expectation = expectation(description: "Wait for view to appear.")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
// Then
|
||||
wait(for: [expectation], timeout: 1)
|
||||
|
||||
assertSnapshot(matching: rootViewController,
|
||||
as: .image(drawHierarchyInKeyWindow: true, precision: Config.precision))
|
||||
}
|
||||
|
||||
+5
-2
@@ -26,6 +26,9 @@ final class GradientLoadingBarViewTestCase: XCTestCase {
|
||||
static let gradientColors = [
|
||||
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
|
||||
].map(Color.init)
|
||||
|
||||
/// The percentage of pixels that must match.
|
||||
static let precision: Float = 0.98
|
||||
}
|
||||
|
||||
// MARK: - Test cases
|
||||
@@ -36,7 +39,7 @@ final class GradientLoadingBarViewTestCase: XCTestCase {
|
||||
.frame(width: Config.frame.width, height: Config.frame.height)
|
||||
|
||||
// Then
|
||||
assertSnapshot(matching: gradientLoadingBarView, as: .image)
|
||||
assertSnapshot(matching: gradientLoadingBarView, as: .image(precision: Config.precision))
|
||||
}
|
||||
|
||||
func test_gradientLoadingBarView_shouldContainCorrectCustomColors() {
|
||||
@@ -45,6 +48,6 @@ final class GradientLoadingBarViewTestCase: XCTestCase {
|
||||
.frame(width: Config.frame.width, height: Config.frame.height)
|
||||
|
||||
// Then
|
||||
assertSnapshot(matching: gradientLoadingBarView, as: .image)
|
||||
assertSnapshot(matching: gradientLoadingBarView, as: .image(precision: Config.precision))
|
||||
}
|
||||
}
|
||||
|
||||
+55
-49
@@ -9,67 +9,73 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension GradientLoadingBarView {
|
||||
/// This view model contains all logic related to the `GradientLoadingBarView`
|
||||
/// and the corresponding progress animation.
|
||||
final class ViewModel: ObservableObject {
|
||||
// See documentation of `GradientLoadingBarView` for further details
|
||||
// on this compile condition.
|
||||
#if arch(arm64) || arch(x86_64)
|
||||
|
||||
// MARK: - Public properties
|
||||
@available(iOS 15.0, *)
|
||||
extension GradientLoadingBarView {
|
||||
/// This view model contains all logic related to the `GradientLoadingBarView`
|
||||
/// and the corresponding progress animation.
|
||||
final class ViewModel: ObservableObject {
|
||||
|
||||
/// The gradient colors used for the progress animation (including the reversed colors).
|
||||
let gradientColors: [Color]
|
||||
// MARK: - Public properties
|
||||
|
||||
/// The horizontal offset of the `LinearGradient` used to simulate the progress animation.
|
||||
@Published
|
||||
private(set) var horizontalOffset: CGFloat = 0
|
||||
/// The gradient colors used for the progress animation (including the reversed colors).
|
||||
let gradientColors: [Color]
|
||||
|
||||
/// The current size of the `GradientLoadingBarView`.
|
||||
@Published
|
||||
var size: CGSize = .zero {
|
||||
didSet {
|
||||
// This will stop any ongoing animation.
|
||||
// Source: https://stackoverflow.com/a/59150940
|
||||
withAnimation(.linear(duration: 0)) {
|
||||
horizontalOffset = -size.width
|
||||
}
|
||||
/// The horizontal offset of the `LinearGradient` used to simulate the progress animation.
|
||||
@Published
|
||||
private(set) var horizontalOffset: CGFloat = 0
|
||||
|
||||
let progressAnimation: Animation = .linear(duration: progressDuration).repeatForever(autoreverses: false)
|
||||
withAnimation(progressAnimation) {
|
||||
horizontalOffset = size.width
|
||||
/// The current size of the `GradientLoadingBarView`.
|
||||
@Published
|
||||
var size: CGSize = .zero {
|
||||
didSet {
|
||||
// This will stop any ongoing animation.
|
||||
// Source: https://stackoverflow.com/a/59150940
|
||||
withAnimation(.linear(duration: 0)) {
|
||||
horizontalOffset = -size.width
|
||||
}
|
||||
|
||||
let progressAnimation: Animation = .linear(duration: progressDuration).repeatForever(autoreverses: false)
|
||||
withAnimation(progressAnimation) {
|
||||
horizontalOffset = size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The width of the `LinearGradient`.
|
||||
var gradientWidth: CGFloat {
|
||||
// To fit `gradientColors + reversedGradientColors + gradientColors` in our view,
|
||||
// we have to apply three times the width of our parent view.
|
||||
size.width * 3
|
||||
}
|
||||
/// The width of the `LinearGradient`.
|
||||
var gradientWidth: CGFloat {
|
||||
// To fit `gradientColors + reversedGradientColors + gradientColors` in our view,
|
||||
// we have to apply three times the width of our parent view.
|
||||
size.width * 3
|
||||
}
|
||||
|
||||
// MARK: - Private properties
|
||||
// MARK: - Private properties
|
||||
|
||||
private var progressDuration: TimeInterval
|
||||
private var progressDuration: TimeInterval
|
||||
|
||||
// MARK: - Instance Lifecycle
|
||||
// MARK: - Instance Lifecycle
|
||||
|
||||
init(gradientColors: [Color], progressDuration: TimeInterval) {
|
||||
// Simulate infinite 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.
|
||||
//
|
||||
// E.g. for array of [.red, .yellow, .green]
|
||||
// we will create [.red, .yellow, .green, .yellow, .red, .yellow, .green]
|
||||
//
|
||||
// E.g. for array of [.red, .yellow, .green, .blue]
|
||||
// we will create [.red, .yellow, .green, .blue, .green, .yellow, .red, .yellow, .green, .blue]
|
||||
let reversedGradientColors = gradientColors
|
||||
.reversed()
|
||||
.dropFirst()
|
||||
.dropLast()
|
||||
init(gradientColors: [Color], progressDuration: TimeInterval) {
|
||||
// Simulate infinite 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.
|
||||
//
|
||||
// E.g. for array of [.red, .yellow, .green]
|
||||
// we will create [.red, .yellow, .green, .yellow, .red, .yellow, .green]
|
||||
//
|
||||
// E.g. for array of [.red, .yellow, .green, .blue]
|
||||
// we will create [.red, .yellow, .green, .blue, .green, .yellow, .red, .yellow, .green, .blue]
|
||||
let reversedGradientColors = gradientColors
|
||||
.reversed()
|
||||
.dropFirst()
|
||||
.dropLast()
|
||||
|
||||
self.gradientColors = gradientColors + reversedGradientColors + gradientColors
|
||||
self.progressDuration = progressDuration
|
||||
self.gradientColors = gradientColors + reversedGradientColors + gradientColors
|
||||
self.progressDuration = progressDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,74 +8,88 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// For some reason the animation looks broken on iOS versions <= 15.0.
|
||||
@available(iOS 15.0, *)
|
||||
public struct GradientLoadingBarView: View {
|
||||
// Workaround to fix `xcodebuild` errors when running `pod lib lint`, like e.g.:
|
||||
//
|
||||
// - `xcodebuild`: error: cannot find type 'Color' in scope
|
||||
// - `xcodebuild`: error: cannot find type 'View' in scope
|
||||
//
|
||||
// > This failure occurs because builds with a deployment target earlier than iOS 11 will also build for the armv7 architecture,
|
||||
// > and there is no armv7 swiftmodule for SwiftUI in the iOS SDK because the OS version in which it was first introduced (iOS 13)
|
||||
// > does not support armv7 anymore.
|
||||
//
|
||||
// Source: https://stackoverflow.com/a/61954608
|
||||
#if arch(arm64) || arch(x86_64)
|
||||
|
||||
// MARK: - Config
|
||||
// For some reason the animation looks broken on iOS versions <= 15.0.
|
||||
@available(iOS 15.0, *)
|
||||
public struct GradientLoadingBarView: View {
|
||||
|
||||
public enum Config {
|
||||
/// The default color palette for the gradient colors.
|
||||
public static let gradientColors = UIColor.GradientLoadingBar.gradientColors.map(Color.init)
|
||||
// MARK: - Config
|
||||
|
||||
/// The default duration for the progress animation, measured in seconds.
|
||||
public static let progressDuration = TimeInterval.GradientLoadingBar.progressDuration
|
||||
}
|
||||
public enum Config {
|
||||
/// The default color palette for the gradient colors.
|
||||
public static let gradientColors = UIColor.GradientLoadingBar.gradientColors.map(Color.init)
|
||||
|
||||
// MARK: - Private properties
|
||||
/// The default duration for the progress animation, measured in seconds.
|
||||
public static let progressDuration = TimeInterval.GradientLoadingBar.progressDuration
|
||||
}
|
||||
|
||||
@StateObject
|
||||
private var viewModel: ViewModel
|
||||
// MARK: - Private properties
|
||||
|
||||
// MARK: - Instance Lifecycle
|
||||
@StateObject
|
||||
private var viewModel: ViewModel
|
||||
|
||||
public init(gradientColors: [Color] = Config.gradientColors,
|
||||
progressDuration: TimeInterval = Config.progressDuration) {
|
||||
// Even though the docs mention that "You don’t call this initializer directly", this seems to be the correct way to set-up a
|
||||
// `StateObject` with parameters according to "Lessons from the SwiftUI Digital Lounge".
|
||||
// https://swiftui-lab.com/random-lessons/#data-10
|
||||
_viewModel = StateObject(
|
||||
wrappedValue: ViewModel(gradientColors: gradientColors, progressDuration: progressDuration)
|
||||
)
|
||||
}
|
||||
// MARK: - Instance Lifecycle
|
||||
|
||||
// MARK: - Render
|
||||
|
||||
public var body: some View {
|
||||
Color.clear
|
||||
// We explicitly have to use a `PreferenceKey` here and store the size on a property in order to restart the animation whenever
|
||||
// the size changes. Using a `GeometryReader` together with the `onAppear(_:)` view-modifier doesn't reflect any size changes.
|
||||
.modifier(SizeModifier())
|
||||
.onPreferenceChange(SizePreferenceKey.self) {
|
||||
viewModel.size = $0
|
||||
}
|
||||
// Using an `overlay` here makes sure that the parent view won't change it's frame.
|
||||
.overlay(
|
||||
LinearGradient(colors: viewModel.gradientColors, startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: viewModel.gradientWidth)
|
||||
.offset(x: viewModel.horizontalOffset, y: 0)
|
||||
public init(gradientColors: [Color] = Config.gradientColors,
|
||||
progressDuration: TimeInterval = Config.progressDuration) {
|
||||
// Even though the docs mention that "You don’t call this initializer directly", this seems to be the correct way to set-up a
|
||||
// `StateObject` with parameters according to "Lessons from the SwiftUI Digital Lounge".
|
||||
// https://swiftui-lab.com/random-lessons/#data-10
|
||||
_viewModel = StateObject(
|
||||
wrappedValue: ViewModel(gradientColors: gradientColors, progressDuration: progressDuration)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Render
|
||||
|
||||
public var body: some View {
|
||||
Color.clear
|
||||
// We explicitly have to use a `PreferenceKey` here and store the size on a property in order to restart the animation whenever
|
||||
// the size changes. Using a `GeometryReader` together with the `onAppear(_:)` view-modifier doesn't reflect any size changes.
|
||||
.modifier(SizeModifier())
|
||||
.onPreferenceChange(SizePreferenceKey.self) {
|
||||
viewModel.size = $0
|
||||
}
|
||||
// Using an `overlay` here makes sure that the parent view won't change it's frame.
|
||||
.overlay(
|
||||
LinearGradient(colors: viewModel.gradientColors, startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: viewModel.gradientWidth)
|
||||
.offset(x: viewModel.horizontalOffset, y: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
// MARK: - Helper
|
||||
|
||||
private struct SizePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGSize = .zero
|
||||
private struct SizePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGSize = .zero
|
||||
|
||||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
||||
value = nextValue()
|
||||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
private struct SizeModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content.background(
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(key: SizePreferenceKey.self,
|
||||
value: geometry.size)
|
||||
}
|
||||
)
|
||||
@available(iOS 15.0, *)
|
||||
private struct SizeModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content.background(
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(key: SizePreferenceKey.self,
|
||||
value: geometry.size)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user