diff --git a/Sources/Pow/Infrastructure/ProgressableAnimation.swift b/Sources/Pow/Infrastructure/ProgressableAnimation.swift new file mode 100644 index 0000000..2ec1fb6 --- /dev/null +++ b/Sources/Pow/Infrastructure/ProgressableAnimation.swift @@ -0,0 +1,62 @@ +// +// ProgressableAnimation.swift +// +// +// Created by Noah Martin on 11/30/23. +// + +import Foundation +import SwiftUI + +#if DEBUG +typealias DebugProgressableAnimation = ProgressableAnimation +#else +typealias DebugProgressableAnimation = Animatable +#endif + +protocol ProgressableAnimation: Animatable { + var progress: CGFloat { get set } +} + +extension ProgressableAnimation where AnimatableData == CGFloat { + var progress: CGFloat { + get { animatableData } + set { animatableData = newValue } + } +} + +#if DEBUG +protocol PreviewableAnimation { + associatedtype Animation: ProgressableAnimation & ViewModifier + + static var animation: Animation { get } + + static var content: any View { get } +} + +extension PreviewableAnimation { + static var content: any View { + RoundedRectangle( + cornerRadius: 8, + style: .continuous) + .fill(Color.blue) + .frame(width: 80, height: 80) + } +} + +extension PreviewableAnimation { + static var previews: AnyView { + let c = self.content + let anyContent = AnyView(c) + let modifiers = [0, 0.25, 0.5, 0.75, 1].map { i in + var copy = self.animation + copy.progress = i + return copy + } + return AnyView(ForEach(Array(modifiers.enumerated()), id: \.offset) { i, modifier in + anyContent.modifier(modifier) + .previewDisplayName("\(String(describing: Animation.self))-\(i)") + }) + } +} +#endif diff --git a/Sources/Pow/Infrastructure/Scaled.swift b/Sources/Pow/Infrastructure/Scaled.swift index bf0ebe3..5890a71 100644 --- a/Sources/Pow/Infrastructure/Scaled.swift +++ b/Sources/Pow/Infrastructure/Scaled.swift @@ -26,3 +26,5 @@ internal struct Scaled: ViewModifier, Animatable { content.modifier(base.animation(nil)) } } + +extension Scaled: ProgressableAnimation where V.AnimatableData == CGFloat { } diff --git a/Sources/Pow/Transitions/Anvil.swift b/Sources/Pow/Transitions/Anvil.swift index 073f629..d53dfaa 100644 --- a/Sources/Pow/Transitions/Anvil.swift +++ b/Sources/Pow/Transitions/Anvil.swift @@ -19,7 +19,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Anvil: ViewModifier, Animatable, AnimatableModifier { +internal struct Anvil: ViewModifier, ProgressableAnimation, AnimatableModifier { var animatableData: CGFloat = 0 #if os(iOS) @@ -31,11 +31,6 @@ internal struct Anvil: ViewModifier, Animatable, AnimatableModifier { self.animatableData = animatableData } - var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { /// Fraction of the animation spent on the view falling down. let fall: CGFloat = 0.1 @@ -212,6 +207,21 @@ extension EdgeInsets { } #if os(iOS) && DEBUG +struct Anvil_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Anvil { + Anvil(animatableData: 0) + } + + static var content: any View { + RoundedRectangle( + cornerRadius: 8, + style: .continuous) + .fill(Color.blue) + .frame(width: 80, height: 80) + .preferredColorScheme(.dark) + } +} + @available(iOS 15.0, *) struct Anvil_Previews: PreviewProvider { struct Item: Identifiable { diff --git a/Sources/Pow/Transitions/Blinds.swift b/Sources/Pow/Transitions/Blinds.swift index cb68bb8..b2ed808 100644 --- a/Sources/Pow/Transitions/Blinds.swift +++ b/Sources/Pow/Transitions/Blinds.swift @@ -30,7 +30,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Blinds: ViewModifier, Animatable, AnimatableModifier, Hashable { +internal struct Blinds: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable { var slatWidth: CGFloat var style: AnyTransition.MovingParts.BlindsStyle @@ -39,11 +39,6 @@ internal struct Blinds: ViewModifier, Animatable, AnimatableModifier, Hashable { var animatableData: CGFloat - private var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { content .mask { @@ -108,6 +103,13 @@ private struct BlindsShape: Shape { } #if os(iOS) && DEBUG +struct Blinds_Preview: PreviewableAnimation & PreviewProvider { + + static var animation: Blinds { + Blinds(slatWidth: 20, style: .venetian, isStaggered: false, animatableData: 0) + } +} + @available(iOS 15.0, *) struct Blinds_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Blur.swift b/Sources/Pow/Transitions/Blur.swift index a19861d..66d1dbd 100644 --- a/Sources/Pow/Transitions/Blur.swift +++ b/Sources/Pow/Transitions/Blur.swift @@ -22,7 +22,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Blur: ViewModifier, Animatable, AnimatableModifier, Hashable { +internal struct Blur: ViewModifier, DebugProgressableAnimation, AnimatableModifier, Hashable { var animatableData: CGFloat { get { radius } set { radius = newValue } @@ -37,6 +37,12 @@ internal struct Blur: ViewModifier, Animatable, AnimatableModifier, Hashable { } #if os(iOS) && DEBUG +struct Blur_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Blur { + Blur(radius: 30) + } +} + @available(iOS 15.0, *) struct Blur_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Boing.swift b/Sources/Pow/Transitions/Boing.swift index 81748fa..0ac51f5 100644 --- a/Sources/Pow/Transitions/Boing.swift +++ b/Sources/Pow/Transitions/Boing.swift @@ -21,7 +21,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Boing: Animatable, GeometryEffect { +internal struct Boing: DebugProgressableAnimation, GeometryEffect { var edge: Edge var animatableData: CGFloat = 0 @@ -107,6 +107,12 @@ internal struct Boing: Animatable, GeometryEffect { } #if os(iOS) && DEBUG +struct Boing_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Scaled { + Scaled(Boing(.top, animatableData: 0)) + } +} + @available(iOS 15.0, *) struct Bounce_Previews: PreviewProvider { struct Item: Identifiable { diff --git a/Sources/Pow/Transitions/Clock.swift b/Sources/Pow/Transitions/Clock.swift index 86a8c28..af7d999 100644 --- a/Sources/Pow/Transitions/Clock.swift +++ b/Sources/Pow/Transitions/Clock.swift @@ -21,7 +21,8 @@ public extension AnyTransition.MovingParts { } } -internal struct Clock: ViewModifier, Animatable, AnimatableModifier { +internal struct Clock: ViewModifier, DebugProgressableAnimation, AnimatableModifier { + var origin: UnitPoint var animatableData: AnimatablePair @@ -32,7 +33,8 @@ internal struct Clock: ViewModifier, Animatable, AnimatableModifier { } var progress: CGFloat { - animatableData.first + get { animatableData.first } + set { animatableData.first = newValue } } var blurRadius: CGFloat { @@ -84,6 +86,12 @@ internal struct Clock: ViewModifier, Animatable, AnimatableModifier { } #if os(iOS) && DEBUG +struct Clock_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Clock { + Clock(origin: .center, blurRadius: 0, progress: 0) + } +} + struct Clock_Previews: PreviewProvider { struct Item: Identifiable { var color: Color diff --git a/Sources/Pow/Transitions/FilmExposure.swift b/Sources/Pow/Transitions/FilmExposure.swift index bb1e3e3..e203b54 100644 --- a/Sources/Pow/Transitions/FilmExposure.swift +++ b/Sources/Pow/Transitions/FilmExposure.swift @@ -20,14 +20,9 @@ public extension AnyTransition.MovingParts { } } -internal struct Snapshot: ViewModifier, Animatable, AnimatableModifier, Hashable { +internal struct Snapshot: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable { var animatableData: CGFloat = 0 - var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { content .saturation(0.5 + 0.5 * clamp(progress)) @@ -38,14 +33,9 @@ internal struct Snapshot: ViewModifier, Animatable, AnimatableModifier, Hashable } } -internal struct ExposureFade: ViewModifier, Animatable, AnimatableModifier, Hashable { +internal struct ExposureFade: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable { var animatableData: CGFloat = 0 - var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { content .opacity(Double(1.0 - pow(2.0, -10.0 * progress))) @@ -55,6 +45,18 @@ internal struct ExposureFade: ViewModifier, Animatable, AnimatableModifier, Hash } #if os(iOS) && DEBUG +struct Snapshot_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Snapshot { + Snapshot(animatableData: 0) + } +} + +struct FilmExposure_Preview: PreviewableAnimation, PreviewProvider { + static var animation: ExposureFade { + ExposureFade(animatableData: 0) + } +} + @available(iOS 15.0, *) struct ExoposureFade_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Flicker.swift b/Sources/Pow/Transitions/Flicker.swift index e09ac44..bf8fe81 100644 --- a/Sources/Pow/Transitions/Flicker.swift +++ b/Sources/Pow/Transitions/Flicker.swift @@ -21,16 +21,11 @@ public extension AnyTransition.MovingParts { } } -internal struct Flicker: ViewModifier, Animatable, AnimatableModifier, Hashable { +internal struct Flicker: ViewModifier, ProgressableAnimation, AnimatableModifier, Hashable { var count: Int var animatableData: CGFloat - private var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - private var isVisible: Bool { (progress * CGFloat(count)).remainder(dividingBy: 1) >= 0 } @@ -43,6 +38,12 @@ internal struct Flicker: ViewModifier, Animatable, AnimatableModifier, Hashable } #if os(iOS) && DEBUG +struct Flicker_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Flicker { + Flicker(count: 1, animatableData: 0) + } +} + @available(iOS 15.0, *) struct Flicker_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Glare.swift b/Sources/Pow/Transitions/Glare.swift index 767aba2..a0ecaa4 100644 --- a/Sources/Pow/Transitions/Glare.swift +++ b/Sources/Pow/Transitions/Glare.swift @@ -47,7 +47,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Glare: ViewModifier, Animatable, AnimatableModifier { +internal struct Glare: ViewModifier, DebugProgressableAnimation, AnimatableModifier { var animatableData: CGFloat = 0 var angle: Angle @@ -132,8 +132,37 @@ internal struct Glare: ViewModifier, Animatable, AnimatableModifier { } #if os(iOS) && DEBUG +@available(iOS 16.0, *) +struct Glare_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Glare { + Glare(.degrees(45), color: .white, increasedBrightness: true, animatableData: 0) + } + + static var content: any View { + Glare_Previews.makeRect(start: .indigo, end: .purple) + .frame(width: 100, height: 100) + .preferredColorScheme(.dark) + } +} + @available(iOS 16.0, *) struct Glare_Previews: PreviewProvider { + static func makeRect(start: Color, end: Color) -> some View { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(LinearGradient( + colors: [start, end], + startPoint: .topLeading, + endPoint: .bottom + )) + .compositingGroup() + .overlay { + Text("Hello\nWorld") + .foregroundStyle(.white.shadow(.inner(radius: 0.5))) + } + .font(.system(.largeTitle, design: .rounded).weight(.medium)) + .multilineTextAlignment(.center) + } + struct Item: Identifiable { var color1: Color var color2: Color @@ -214,19 +243,7 @@ struct Glare_Previews: PreviewProvider { ForEach(items.indices, id: \.self) { index in let item = items[index] - RoundedRectangle(cornerRadius: 18, style: .continuous) - .fill(LinearGradient( - colors: [item.color1, item.color2], - startPoint: .topLeading, - endPoint: .bottom - )) - .compositingGroup() - .overlay { - Text("Hello\nWorld") - .foregroundStyle(.white.shadow(.inner(radius: 0.5))) - } - .font(.system(.largeTitle, design: .rounded).weight(.medium)) - .multilineTextAlignment(.center) + Glare_Previews.makeRect(start: item.color1, end: item.color2) .transition( .asymmetric( insertion: .movingParts.glare(angle: angle), diff --git a/Sources/Pow/Transitions/Iris.swift b/Sources/Pow/Transitions/Iris.swift index 7c069c3..cb1ac20 100644 --- a/Sources/Pow/Transitions/Iris.swift +++ b/Sources/Pow/Transitions/Iris.swift @@ -15,7 +15,7 @@ public extension AnyTransition.MovingParts { } } -private struct Iris: ViewModifier, Animatable, AnimatableModifier { +struct Iris: ViewModifier, DebugProgressableAnimation, AnimatableModifier { var origin: UnitPoint var blurRadius: CGFloat @@ -58,6 +58,12 @@ private struct Iris: ViewModifier, Animatable, AnimatableModifier { } #if os(iOS) && DEBUG +struct Iris_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Iris { + Iris(origin: .center, animatableData: 0) + } +} + @available(iOS 15.0, *) struct Mask_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Poof.swift b/Sources/Pow/Transitions/Poof.swift index 8744489..400d9fa 100644 --- a/Sources/Pow/Transitions/Poof.swift +++ b/Sources/Pow/Transitions/Poof.swift @@ -16,18 +16,13 @@ public extension AnyTransition.MovingParts { } } -private struct Poof: ViewModifier, Animatable, AnimatableModifier { +struct Poof: ViewModifier, ProgressableAnimation, AnimatableModifier { var animatableData: CGFloat = 0 internal init(animatableData: CGFloat) { self.animatableData = animatableData } - var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { let frame = (6 * progress).rounded() @@ -56,6 +51,25 @@ private struct Poof: ViewModifier, Animatable, AnimatableModifier { } #if os(iOS) && DEBUG +struct Proof_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Poof { + Poof(animatableData: 0) + } + + static var content: any View { + ZStack { + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(Color.accentColor) + + Text("Hello\nWorld!") + .foregroundColor(.white) + .multilineTextAlignment(.center) + .font(.system(.title, design: .rounded)) + } + .frame(width: 300, height: 150) + } +} + @available(iOS 15.0, *) struct Poof_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Pop.swift b/Sources/Pow/Transitions/Pop.swift index 0a7c42b..767411c 100644 --- a/Sources/Pow/Transitions/Pop.swift +++ b/Sources/Pow/Transitions/Pop.swift @@ -51,7 +51,7 @@ public extension AnyTransition.MovingParts { } @available(iOS 15.0, *) -private struct Pop: AnimatableModifier, Animatable, ViewModifier { +struct Pop: AnimatableModifier, ProgressableAnimation, ViewModifier { var animatableData: CGFloat = 0 var style: AnyShapeStyle @@ -63,11 +63,6 @@ private struct Pop: AnimatableModifier, Animatable, ViewModifier { self.style = style } - var progress: CGFloat { - get { animatableData } - set { animatableData = newValue } - } - func body(content: Content) -> some View { let t = clamp(2 * (progress - 1/2.5)) @@ -170,6 +165,19 @@ private struct Pop: AnimatableModifier, Animatable, ViewModifier { } #if os(iOS) && DEBUG +struct Pop_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Pop { + Pop(style: AnyShapeStyle(.tint), animatableData: 0) + } + + static var content: any View { + Image(systemName: "heart.fill") + .foregroundColor(.red) + .tint(.red) + .preferredColorScheme(.dark) + } +} + @available(iOS 15.0, *) struct Pop_Previews: PreviewProvider { struct Preview: View { diff --git a/Sources/Pow/Transitions/Skid.swift b/Sources/Pow/Transitions/Skid.swift index b530d6e..95f1b94 100644 --- a/Sources/Pow/Transitions/Skid.swift +++ b/Sources/Pow/Transitions/Skid.swift @@ -29,7 +29,7 @@ public extension AnyTransition.MovingParts { } } -internal struct Skid: Animatable, GeometryEffect { +internal struct Skid: DebugProgressableAnimation, GeometryEffect { var direction: AnyTransition.MovingParts.SkidDirection var animatableData: CGFloat = 0 @@ -73,6 +73,29 @@ internal struct Skid: Animatable, GeometryEffect { } #if os(iOS) && DEBUG +struct Skid_Preview: PreviewableAnimation, PreviewProvider { + static var animation: Skid { + Skid(.leading) + } + + static var content: some View { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.orange) + .overlay { + Text("Jell-O\nWorld") + .blendMode(.difference) + .offset(x: 2, y: 2) + } + .compositingGroup() + .overlay { + Text("Jell-O\nWorld") + } + .font(.system(.headline, design: .rounded).weight(.black)) + .multilineTextAlignment(.center) + .frame(width: 150, height: 150) + } +} + @available(iOS 15.0, *) struct Skid_Previews: PreviewProvider { struct Item: Identifiable {