From 95ae26b55b7326928b82f6c06a47bdac0bd2eb61 Mon Sep 17 00:00:00 2001 From: Vladyslav Sosiuk Date: Mon, 6 Nov 2023 11:18:07 -0800 Subject: [PATCH] fix: Remove IfElse To Keep State Correct Refs: #12 --- Sources/Shimmer/Shimmer.swift | 72 ++++++++++++++++--- .../Utility/Gradient+Convenience.swift | 14 ++++ .../Shimmer/Utility/View+iOS13Support.swift | 22 ++++++ 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 Sources/Shimmer/Utility/Gradient+Convenience.swift create mode 100644 Sources/Shimmer/Utility/View+iOS13Support.swift diff --git a/Sources/Shimmer/Shimmer.swift b/Sources/Shimmer/Shimmer.swift index de93ea7..efe1a6b 100644 --- a/Sources/Shimmer/Shimmer.swift +++ b/Sources/Shimmer/Shimmer.swift @@ -8,6 +8,7 @@ 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 { + private let isActive: Bool private let animation: Animation private let gradient: Gradient private let min, max: CGFloat @@ -21,10 +22,12 @@ public struct Shimmer: ViewModifier { /// - 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( + isActive: Bool, animation: Animation = Self.defaultAnimation, gradient: Gradient = Self.defaultGradient, bandSize: CGFloat = 0.3 ) { + self.isActive = isActive self.animation = animation self.gradient = gradient // Calculate unit point dimensions beyond the gradient's edges by the band size @@ -82,11 +85,37 @@ public struct Shimmer: ViewModifier { public func body(content: Content) -> some View { content - .mask(LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)) - .animation(animation, value: isInitialState) + .mask( + LinearGradient( + gradient: isActive ? gradient : .transparent, + startPoint: startPoint, + endPoint: endPoint + ) + ) + .animation( + isActive ? animation : .linear(duration: 0), // FW: This is a correct way to stop animation. If using `isActive ? .linear(...) : .none` instead then the animation would be choppy. https://stackoverflow.com/questions/61830571/whats-causing-swiftui-nested-view-items-jumpy-animation-after-the-initial-drawi/61841018#61841018 + value: isInitialState + ) .onAppear { - isInitialState = false + if isActive { + startShimmering() + } } + .valueChanged(value: isActive) { isActive in + if isActive { + startShimmering() + } else { + stopShimmering() + } + } + } + + private func startShimmering() { + isInitialState = false + } + + private func stopShimmering() { + isInitialState = true } } @@ -104,11 +133,9 @@ public extension View { gradient: Gradient = Shimmer.defaultGradient, bandSize: CGFloat = 0.3 ) -> some View { - if active { - modifier(Shimmer(animation: animation, gradient: gradient, bandSize: bandSize)) - } else { - self - } + modifier( + Shimmer(isActive: active, animation: animation, gradient: gradient, bandSize: bandSize) + ) } /// Adds an animated shimmering effect to any view, typically to show that an operation is in progress. @@ -130,6 +157,32 @@ public extension View { #if DEBUG struct Shimmer_Previews: PreviewProvider { + struct TogglePreview: View { + @State private var isShimmeringActive = true + + var body: some View { + Form { + Toggle(isOn: $isShimmeringActive) { + Text("Is shimmering active") + } + ViewWithItsOwnState() + .shimmering(active: isShimmeringActive) + } + } + + struct ViewWithItsOwnState: View { + @State private var isOn: Bool = false + + var body: some View { + Toggle(isOn: $isOn) { + Text("Should remain the same when toggle shimmering") + } + Text("SwiftUI Shimmer") + .foregroundColor(.red) + } + } + } + static var previews: some View { Group { Text("SwiftUI Shimmer") @@ -155,6 +208,9 @@ struct Shimmer_Previews: PreviewProvider { .font(.largeTitle) .shimmering() .environment(\.layoutDirection, .rightToLeft) + + TogglePreview() + .previewDisplayName("Shimmering toggle") } } #endif diff --git a/Sources/Shimmer/Utility/Gradient+Convenience.swift b/Sources/Shimmer/Utility/Gradient+Convenience.swift new file mode 100644 index 0000000..46f3191 --- /dev/null +++ b/Sources/Shimmer/Utility/Gradient+Convenience.swift @@ -0,0 +1,14 @@ +// +// Gradient+Convenience.swift +// +// +// Created by Vladyslav Sosiuk on 06.11.2023. +// + +import SwiftUI + +extension Gradient { + static var transparent: Self { + .init(colors: [.black]) // .black has no effect + } +} diff --git a/Sources/Shimmer/Utility/View+iOS13Support.swift b/Sources/Shimmer/Utility/View+iOS13Support.swift new file mode 100644 index 0000000..1560e9a --- /dev/null +++ b/Sources/Shimmer/Utility/View+iOS13Support.swift @@ -0,0 +1,22 @@ +// +// View+iOS13Support.swift +// +// +// Created by Vladyslav Sosiuk on 06.11.2023. +// + +import SwiftUI +import Combine + +extension View { + /// A backwards compatible wrapper for iOS 14 `onChange` based on https://betterprogramming.pub/implementing-swiftui-onchange-support-for-ios13-577f9c086c9 + @ViewBuilder func valueChanged(value: T, onChange: @escaping (T) -> Void) -> some View { + if #available(iOS 14.0, *) { + self.onChange(of: value, perform: onChange) + } else { + self.onReceive(Just(value)) { (value) in + onChange(value) + } + } + } +}