I want to create a shake animation when the User presses the "save"-button and the input is not valid. My first approach is this (to simplify I removed the modifiers and not for this case relevant attributes):
View:
struct CreateDeckView: View {
@StateObject var viewModel = CreateDeckViewModel()
HStack {
TextField("Enter title", text: $viewModel.title)
.offset(x: viewModel.isValid ? 0 : 10) //
.animation(Animation.default.repeatCount(5).speed(4)) // shake animation
Button(action: {
viewModel.buttonPressed = true
viewModel.saveDeck(){
self.presentationMode.wrappedValue.dismiss()
}
}, label: {
Text("Save")
})
}
}
ViewModel:
class CreateDeckViewModel: ObservableObject{
@Published var title: String = ""
@Published var buttonPressed = false
var validTitle: Bool {
buttonPressed && !(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: @escaping () -> ()){ ... }
}
But this solution doesn't really work. For the first time when I press the button nothing happens. After that when I change the textfield it starts to shake.
using GeometryEffect,
struct ContentView: View {
@StateObject var viewModel = CreateDeckViewModel()
var body: some View {
HStack {
TextField("Enter title", text: $viewModel.title)
.modifier(ShakeEffect(shakes: viewModel.shouldShake ? 2 : 0)) //<- here
.animation(Animation.default.repeatCount(6).speed(3))
Button(action: {
viewModel.saveDeck(){
...
}
}, label: {
Text("Save")
})
}
}
}
//here
struct ShakeEffect: GeometryEffect {
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(CGAffineTransform(translationX: -30 * sin(position * 2 * .pi), y: 0))
}
init(shakes: Int) {
position = CGFloat(shakes)
}
var position: CGFloat
var animatableData: CGFloat {
get { position }
set { position = newValue }
}
}
class CreateDeckViewModel: ObservableObject{
@Published var title: String = ""
@Published var shouldShake = false
var validTitle: Bool {
!(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: @escaping () -> ()){
if !validTitle {
shouldShake.toggle() //<- here (you can use PassThrough subject insteadof toggling.)
}
}
}
I couldn't identify the issue that prevented Okayokay solution from working correctly on (iOS 17, *). Therefore, I developed my own variant based on the ideas presented above. It works smoothly on iOS 17. I hope this helps
Full Code:
import SwiftUI
import Combine
class ShakeViewModel: ObservableObject {
var shake = PassthroughSubject<Void, Never>()
func needShake() {
shake.send()
}
}
extension View {
func shakeAnimation(_ shake: Binding<Bool>, sink: PassthroughSubject<Void, Never>, intensity: CGFloat = 3, duration: CGFloat = 0.05) -> some View {
modifier(ShakeEffect(shake: shake, sink: sink, intensity: intensity, duration: duration))
}
}
struct ShakeEffect: ViewModifier {
@Binding var shake: Bool
var sink: PassthroughSubject<Void, Never>
let intensity: CGFloat
let duration: CGFloat
func body(content: Content) -> some View {
content
.modifier(ShakeViewModifier(shake: $shake, sink: sink, intensity: intensity, duration: duration))
}
}
struct ShakeViewModifier: ViewModifier {
@Binding var shake: Bool
var sink: PassthroughSubject<Void, Never>
let intensity: CGFloat
let duration: CGFloat
@State private var xIntensity: CGFloat = 0
func body(content: Content) -> some View {
content
.offset(x: shake ? xIntensity : -xIntensity, y: 0)
.onReceive(sink) { _ in
self.xIntensity = intensity
withAnimation(.easeInOut(duration: duration).repeatCount(5)) {
shake.toggle()
} completion: {
withAnimation(.easeInOut(duration: duration)) {
self.xIntensity = 0
}
}
}
}
}
struct ShakeView: View {
@StateObject var viewModel: ShakeViewModel = ShakeViewModel()
@State var shakeAnimation: Bool = false
var body: some View {
ZStack {
Rectangle()
.fill(Color.purple)
.cornerRadius(10)
.frame(width: 200, height: 200)
.shakeAnimation($shakeAnimation, sink: viewModel.shake, intensity: 6, duration: 0.06)
.onTapGesture {
viewModel.needShake()
}
}
}
}
struct ShakeView_Previews: PreviewProvider {
static var previews: some View {
ShakeView()
}
}

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