SwiftUI:输入无效时文本字段摇动动画

SwiftUI: Textfield shake animation when input is not valid

我想在用户按下“保存”按钮且输入无效时创建摇动动画。我的第一种方法是这样的(为了简化我删除了修饰符而不是这种情况下的相关属性):

查看:

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")
                })
         }
}

视图模型:

class CreateDeckViewModel: ObservableObject{

    @Published var title: String = ""
    @Published var buttonPressed = false

    var validTitle: Bool {
        buttonPressed && !(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
    }

    public func saveDeck(completion: @escaping () -> ()){ ... }
}
             

但是这个解决方案并没有真正起作用。当我第一次按下按钮时,什么也没有发生。之后,当我更改文本字段时,它开始摇晃。

使用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.)
            }
        }
    }