HuggingChat-Mac/Animations/Shimmer.swift (126 lines of code) (raw):

// // Shimmer.swift // HuggingChat-Mac // // Created by Cyril Zakka on 11/30/24. // import SwiftUI /// A view modifier that applies an animated "shimmer" to any view, typically to show that an operation is in progress. public struct Shimmer: ViewModifier { public enum Mode { /// Masks the content with the gradient (this is the usual, default mode). case mask /// Overlays the gradient with a given `BlendMode` (`.sourceAtop` by default). case overlay(blendMode: BlendMode = .sourceAtop) /// Places the gradient behind the content. case background } private let animation: Animation private let gradient: Gradient private let min, max: CGFloat private let mode: Mode @State private var isInitialState = true @Environment(\.layoutDirection) private var layoutDirection /// Initializes his modifier with a custom animation, /// - Parameters: /// - animation: A custom animation. Defaults to ``Shimmer/defaultAnimation``. /// - gradient: A custom gradient. Defaults to ``Shimmer/defaultGradient``. /// - bandSize: The size of the animated mask's "band". Defaults to 0.3 unit points, which corresponds to /// 30% of the extent of the gradient. public init( animation: Animation = Self.defaultAnimation, gradient: Gradient = Self.defaultGradient, bandSize: CGFloat = 0.3, mode: Mode = .mask ) { self.animation = animation self.gradient = gradient // Calculate unit point dimensions beyond the gradient's edges by the band size self.min = 0 - bandSize self.max = 1 + bandSize self.mode = mode } /// The default animation effect. public static let defaultAnimation = Animation.linear(duration: 1.5).delay(0.25).repeatForever(autoreverses: false) // A default gradient for the animated mask. public static let defaultGradient = Gradient(colors: [ .black.opacity(0.3), // translucent .black, // opaque .black.opacity(0.3) // translucent ]) /* Calculating the gradient's animated start and end unit points: min,min \ ┌───────┐ ┌───────┐ │0,0 │ Animate │ │ "forward" gradient LTR │ │ ───────►│ 1,1│ / // / └───────┘ └───────┘ \ max,max max,min / ┌───────┐ ┌───────┐ │ 1,0│ Animate │ │ "backward" gradient RTL │ │ ───────►│0,1 │ \ \\ \ └───────┘ └───────┘ / min,max */ /// The start unit point of our gradient, adjusting for layout direction. var startPoint: UnitPoint { if layoutDirection == .rightToLeft { isInitialState ? UnitPoint(x: max, y: min) : UnitPoint(x: 0, y: 1) } else { isInitialState ? UnitPoint(x: min, y: min) : UnitPoint(x: 1, y: 1) } } /// The end unit point of our gradient, adjusting for layout direction. var endPoint: UnitPoint { if layoutDirection == .rightToLeft { isInitialState ? UnitPoint(x: 1, y: 0) : UnitPoint(x: min, y: max) } else { isInitialState ? UnitPoint(x: 0, y: 0) : UnitPoint(x: max, y: max) } } public func body(content: Content) -> some View { applyingGradient(to: content) .animation(animation, value: isInitialState) .onAppear { // Delay the animation until the initial layout is established // to prevent animating the appearance of the view DispatchQueue.main.asyncAfter(deadline: .now()) { isInitialState = false } } } @ViewBuilder public func applyingGradient(to content: Content) -> some View { let gradient = LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint) switch mode { case .mask: content.mask(gradient) case let .overlay(blendMode: blendMode): content.overlay(gradient.blendMode(blendMode)) case .background: content.background(gradient) } } } public extension View { /// Adds an animated shimmering effect to any view, typically to show that an operation is in progress. /// - Parameters: /// - active: Convenience parameter to conditionally enable the effect. Defaults to `true`. /// - animation: A custom animation. Defaults to ``Shimmer/defaultAnimation``. /// - gradient: A custom gradient. Defaults to ``Shimmer/defaultGradient``. /// - bandSize: The size of the animated mask's "band". Defaults to 0.3 unit points, which corresponds to /// 20% of the extent of the gradient. @ViewBuilder func shimmering( active: Bool = true, animation: Animation = Shimmer.defaultAnimation, gradient: Gradient = Shimmer.defaultGradient, bandSize: CGFloat = 0.3, mode: Shimmer.Mode = .mask ) -> some View { if active { modifier(Shimmer(animation: animation, gradient: gradient, bandSize: bandSize, mode: mode)) } else { self } } /// Adds an animated shimmering effect to any view, typically to show that an operation is in progress. /// - Parameters: /// - active: Convenience parameter to conditionally enable the effect. Defaults to `true`. /// - duration: The duration of a shimmer cycle in seconds. /// - bounce: Whether to bounce (reverse) the animation back and forth. Defaults to `false`. /// - delay:A delay in seconds. Defaults to `0.25`. @available(*, deprecated, message: "Use shimmering(active:animation:gradient:bandSize:) instead.") @ViewBuilder func shimmering( active: Bool = true, duration: Double, bounce: Bool = false, delay: Double = 0.25 ) -> some View { shimmering( active: active, animation: .linear(duration: duration).delay(delay).repeatForever(autoreverses: bounce) ) } } #if DEBUG struct Shimmer_Previews: PreviewProvider { static var previews: some View { Group { Text("SwiftUI Shimmer") if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { Text("SwiftUI Shimmer").preferredColorScheme(.light) Text("SwiftUI Shimmer").preferredColorScheme(.dark) VStack(alignment: .leading) { Text("Loading...").font(.title) Text(String(repeating: "Shimmer", count: 12)) .redacted(reason: .placeholder) }.frame(maxWidth: 200) } } .padding() .shimmering() .previewLayout(.sizeThatFits) VStack(alignment: .leading) { Text("مرحبًا") Text("← Right-to-left layout direction").font(.body) Text("שלום") } .font(.largeTitle) .shimmering() .environment(\.layoutDirection, .rightToLeft) Text("Custom Gradient Mode").bold() .font(.largeTitle) .shimmering( gradient: Gradient(colors: [.clear, .orange, .white, .green, .clear]), bandSize: 0.5, mode: .overlay() ) } } #endif