SwiftUI: анимация встряхивания текстового поля при недопустимом вводе
Я хочу создать анимацию встряхивания, когда пользователь нажимает кнопку «сохранить», а ввод недопустим. Мой первый подход таков (для упрощения я удалил модификаторы, а не соответствующие атрибуты в этом случае):
Вид:
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 () -> ()){ ... }
}
Но это решение не работает. В первый раз при нажатии на кнопку ничего не происходит. После этого, когда я меняю текстовое поле, оно начинает трястись.
2 ответа
Решение
с помощью
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.)
}
}
}
По ответу @YodagamaHeshan, это мой способ, я думаю, его легко использовать повторно:
public struct ShakeEffect: GeometryEffect {
public var amount: CGFloat = 10
public var shakesPerUnit = 3
public var animatableData: CGFloat
public init(amount: CGFloat = 10, shakesPerUnit: Int = 3, animatableData: CGFloat) {
self.amount = amount
self.shakesPerUnit = shakesPerUnit
self.animatableData = animatableData
}
public func effectValue(size: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0))
}
}
extension View {
public func shakeAnimation(_ shake: Binding<Bool>, sink: PassthroughSubject<Void, Never>) -> some View {
modifier(ShakeEffect(animatableData: shake.wrappedValue ? 2 : 0))
.animation(.default, value: shake.wrappedValue)
.onReceive(sink) {
shake.wrappedValue = true
withAnimation(.default.delay(0.15)) { shake.wrappedValue = false }
}
}
}
Там, где вы хотите встряхнуться, просто сделайте вот так
/// In View
@State var shakeAnimation: Bool = false
VStack { /// any view
....
}
.shakeAnimation($shake, sink: model.shake)
/// In Model
var shake = PassthroughSubject<Void, Never>()
....
func needShake() { shake.send() }