Simple and regular approach to animate a bump effect for a button but not simple in SwiftUI.
I'm trying to change scale
in tapGesture
modifier, but it doesn't have any effect. I don't know how to make chain of animations, probably because SwiftUI doesn't have it. So my naive approach was:
@State private var scaleValue = CGFloat(1)
...
Button(action: {
withAnimation {
self.scaleValue = 1.5
}
withAnimation {
self.scaleValue = 1.0
}
}) {
Image("button1")
.scaleEffect(self.scaleValue)
}
Obviously it doesn't work and buttons image get last scale value immediately.
My second thought was to change scale to 0.8
value on hold
event and then after release
event make scale to 1.2
and again after few mseconds change it to 1.0
. I guess this algorithm should make nice and more natural bump effect. But I couldn't find suitable gesture
struct in SwiftUI to handle hold-n-release
event.
P.S. For ease understanding, I will describe the steps of the hold-n-release
algorithm:
1.0
0.8
1.2
0.1
sec1.0
UPD: I found a simple solution using animation delay
modifier. But I'm not sure it's right and clear. Also it doens't cover hold-n-release
issue:
@State private var scaleValue = CGFloat(1)
...
Button(action: {
withAnimation {
self.scaleValue = 1.5
}
//
// Using delay for second animation block
//
withAnimation(Animation.linear.delay(0.2)) {
self.scaleValue = 1.0
}
}) {
Image("button1")
.scaleEffect(self.scaleValue)
}
UPD 2:
I noticed in solution above it doesn't matter what value I pass as argument to delay
modifier: 0.2
or 1000
will have same effect. Perhaps it's a bug 🐞
So I've used Timer
instance instead of delay
animation modifier. And now it's working as expected:
...
Button(action: {
withAnimation {
self.scaleValue = 1.5
}
//
// Replace it
//
// withAnimation(Animation.linear.delay(0.2)) {
// self.scaleValue = 1.0
// }
//
// by Timer with 0.5 msec delay
//
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
withAnimation {
self.scaleValue = 1.0
}
}
}) {
...
UPD 3:
Until we waiting official Apple update, one of suitable solution for realization of two events touchStart
and touchEnd
is based on @average Joe answer:
import SwiftUI
struct TouchGestureViewModifier: ViewModifier {
let minimumDistance: CGFloat
let touchBegan: () -> Void
let touchEnd: (Bool) -> Void
@State private var hasBegun = false
@State private var hasEnded = false
init(minimumDistance: CGFloat, touchBegan: @escaping () -> Void, touchEnd: @escaping (Bool) -> Void) {
self.minimumDistance = minimumDistance
self.touchBegan = touchBegan
self.touchEnd = touchEnd
}
private func isTooFar(_ translation: CGSize) -> Bool {
let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
return distance >= minimumDistance
}
func body(content: Content) -> some View {
content.gesture(DragGesture(minimumDistance: 0)
.onChanged { event in
guard !self.hasEnded else { return }
if self.hasBegun == false {
self.hasBegun = true
self.touchBegan()
} else if self.isTooFar(event.translation) {
self.hasEnded = true
self.touchEnd(false)
}
}
.onEnded { event in
if !self.hasEnded {
let success = !self.isTooFar(event.translation)
self.touchEnd(success)
}
self.hasBegun = false
self.hasEnded = false
}
)
}
}
extension View {
func onTouchGesture(minimumDistance: CGFloat = 20.0,
touchBegan: @escaping () -> Void,
touchEnd: @escaping (Bool) -> Void) -> some View {
modifier(TouchGestureViewModifier(minimumDistance: minimumDistance, touchBegan: touchBegan, touchEnd: touchEnd))
}
}
We are using UIPinchGestureRecognizer for pinch to zoom functionality. TapGesture for Double Tap to zoom and reset. We can pass minScale and maxScale to limit the zoom in and zoom out scale.
If you want a SwiftUI view to start animating as soon as it appears, you should use the onAppear() modifier to attach an animation.
SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.
struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 2 : 1)
}
}
struct Test2View: View {
var body: some View {
Button(action: {}) {
Image("button1")
}.buttonStyle(ScaleButtonStyle())
}
}
Nice and clean swiftUI
solution:
@State private var scaleValue = CGFloat(1)
...
Image("button1")
.scaleEffect(self.scaleValue)
.onTouchGesture(
touchBegan: { withAnimation { self.scaleValue = 1.5 } },
touchEnd: { _ in withAnimation { self.scaleValue = 1.0 } }
)
however, you also need to add this code snippet to the project:
struct TouchGestureViewModifier: ViewModifier {
let touchBegan: () -> Void
let touchEnd: (Bool) -> Void
@State private var hasBegun = false
@State private var hasEnded = false
private func isTooFar(_ translation: CGSize) -> Bool {
let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
return distance >= 20.0
}
func body(content: Content) -> some View {
content.gesture(DragGesture(minimumDistance: 0)
.onChanged { event in
guard !self.hasEnded else { return }
if self.hasBegun == false {
self.hasBegun = true
self.touchBegan()
} else if self.isTooFar(event.translation) {
self.hasEnded = true
self.touchEnd(false)
}
}
.onEnded { event in
if !self.hasEnded {
let success = !self.isTooFar(event.translation)
self.touchEnd(success)
}
self.hasBegun = false
self.hasEnded = false
})
}
}
extension View {
func onTouchGesture(touchBegan: @escaping () -> Void,
touchEnd: @escaping (Bool) -> Void) -> some View {
modifier(TouchGestureViewModifier(touchBegan: touchBegan, touchEnd: touchEnd))
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With