Skip to content

Instantly share code, notes, and snippets.

@aswathr
Last active September 7, 2022 15:16
Show Gist options
  • Save aswathr/417734430e00dbe1a29f17ebf58a94f5 to your computer and use it in GitHub Desktop.
Save aswathr/417734430e00dbe1a29f17ebf58a94f5 to your computer and use it in GitHub Desktop.
ZoomInOutOnTapModifier
//Button scaling animation test
//Supporting GIST for https://stackoverflow.com/a/73637703/3970488
import SwiftUI
import PlaygroundSupport
/// An animatable modifier that is used for observing animations for a given animatable value.
public struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic {
/// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes.
public var animatableData: Value {
didSet {
notifyCompletionIfFinished()
}
}
/// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes.
private var targetValue: Value
/// The completion callback which is called once the animation completes.
private var completion: () -> Void
init(observedValue: Value, completion: @escaping () -> Void) {
self.completion = completion
self.animatableData = observedValue
targetValue = observedValue
}
/// Verifies whether the current animation is finished and calls the completion callback if true.
private func notifyCompletionIfFinished() {
guard animatableData == targetValue else { return }
/// Dispatching is needed to take the next runloop for the completion callback.
/// This prevents errors like "Modifying state during view update, this will cause undefined behavior."
DispatchQueue.main.async {
self.completion()
}
}
public func body(content: Content) -> some View {
/// We're not really modifying the view so we can directly return the original input value.
return content
}
}
public extension View {
/// Calls the completion handler whenever an animation on the given value completes.
/// - Parameters:
/// - value: The value to observe for animations.
/// - completion: The completion callback to call once the animation completes.
/// - Returns: A modified `View` instance with the observer attached.
func onAnimationCompleted<Value: VectorArithmetic>(for value: Value, completion: @escaping () -> Void) -> ModifiedContent<Self, AnimationCompletionObserverModifier<Value>> {
return modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion))
}
}
struct ZoomInOutOnTapModifier: ViewModifier {
var destinationScaleFactor: CGFloat
var duration: TimeInterval
init(duration: TimeInterval = 0.3,
destinationScaleFactor: CGFloat = 1.2) {
self.duration = duration
self.destinationScaleFactor = destinationScaleFactor
}
@State var scale: CGFloat = 1
@State var secondHalfAnimationStarted = false
@State var animationCompleted = false
func body(content: Content) -> some View {
content
.scaleEffect(scale)
.simultaneousGesture(DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
.onChanged({ _ in
animationCompleted = true
withAnimation(.linear(duration: duration)) {
scale = destinationScaleFactor
}
})
.onEnded({ _ in
withAnimation(.linear(duration: duration)) {
scale = 1
}
secondHalfAnimationStarted = true
})
)
.onAnimationCompleted(for: scale) {
if scale == 1 {
secondHalfAnimationStarted = false
animationCompleted = true } else if scale == destinationScaleFactor {
animationCompleted = false
secondHalfAnimationStarted = true
}
if !secondHalfAnimationStarted {
withAnimation(.linear(duration: duration)) {
scale = 1
}
}
}
}
}
//struct ZoomInOutOnTapModifier: ViewModifier {
//
// var destinationScaleFactor: CGFloat
// var duration: TimeInterval
//
// init(duration: TimeInterval = 0.3,
// destinationScaleFactor: CGFloat = 1.2) {
//
// self.duration = duration
// self.destinationScaleFactor = destinationScaleFactor
// }
//
// @State var scale: CGFloat = 1
//
// func body(content: Content) -> some View {
//
// content
// .scaleEffect(scale)
// .simultaneousGesture(DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
// .onChanged({ _ in
//
// withAnimation(.linear(duration: duration)) {
// scale = destinationScaleFactor
// }
// })
// .onEnded({ _ in
// withAnimation(.linear(duration: duration)) {
// scale = 1
// }
// })
// )
// }
//}
public extension View {
func addingZoomOnTap(duration: TimeInterval = 0.3, destinationScaleFactor: CGFloat = 1.2) -> some View {
modifier(ZoomInOutOnTapModifier(duration: duration, destinationScaleFactor: destinationScaleFactor))
}
}
PlaygroundPage.current.setLiveView(
Button {
print("Button tapped")
} label: {
Text("Tap me")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(Capsule()
.fill(Color.black))
}
.addingZoomOnTap()
.frame(width: 300, height: 300)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment