Last active
September 7, 2022 15:16
-
-
Save aswathr/417734430e00dbe1a29f17ebf58a94f5 to your computer and use it in GitHub Desktop.
ZoomInOutOnTapModifier
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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