在 SwiftUI 中粘贴文本时,如何使 OTP 中的线条动画一次移动一个

How to make animation of lines in OTP move one at a time when pasting a text in SwiftUI

我创建了一个 OTP 字段,它会在您每次键入数字时为线条设置动画。这很好用。但是,我希望在粘贴时将绿线的动画一次 运行 一条(从左到右),就像键入数字时一样。

目前,当您粘贴文本时,所有行都会同时动画,如视频中所示。

预计应该是粘贴时从左到右一条线变绿

请注意,我的粘贴按钮是我创建的自定义按钮,作为其他一些功能的解决方法。

我也只能用Xcode12.3.

这是我的代码:

一些十六进制颜色和字体是自定义的。复制的时候可以替换。


@available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
    
    var numberOfFields: Int
    
    init(numberOfFields: Int = 6) {
        self.numberOfFields = numberOfFields
    }
    
    @Published var otpField = "" {
        didSet {
            showPasteButton = false
            guard otpField.last?.isNumber ?? true else {
                otpField = oldValue
                return
            }
            if otpField.count == numberOfFields {
                showPasteButton = false
            }
        }
    }
    
    @Published var isEditing = false {
        didSet {
            if !isEditing { showPasteButton = isEditing }
        }
    }
    
    @Published var showPasteButton = false
    
    func otp(digit: Int) -> String {
        guard otpField.count >= digit else {
            return ""
        }
        return String(Array(otpField)[digit - 1])
    }
    
    private func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

@available(iOS 13.0, *)
struct CXLRPOTPView: View {
    @ObservedObject var viewModel = OTPViewModel()
    @Environment(\.colorScheme) var colorScheme
    
    private let textBoxWidth: CGFloat = 41
    private let textBoxHeight = UIScreen.main.bounds.width / 8
    private let spaceBetweenLines: CGFloat = 16
    private let paddingOfBox: CGFloat = 1
    private var textFieldOriginalWidth: CGFloat {
        (textBoxWidth + CGFloat(18)) * CGFloat(viewModel.numberOfFields)
    }
    var body: some View {
        VStack {
            ZStack {
                // DOUBLE TAP AND LONG PRESS LISTENER
                Text("123456")
                    .onTapGesture(count: 2) {
                        viewModel.showPasteButton = true
                    }
                    .frame(width: textFieldOriginalWidth, height: textBoxHeight)
                    .background(Color.clear)
                    .font(Font.system(size: 90, design: .default))
                    .foregroundColor(Color.clear)
                    .onLongPressGesture(minimumDuration: 0.5) {
                        self.viewModel.showPasteButton = true
                    }
                
                // OTP TEXT
                HStack (spacing: spaceBetweenLines) {
                    ForEach(1 ... viewModel.numberOfFields, id: \.self) { digit in
                        otpText(
                            text: viewModel.otp(digit: digit),
                            isEditing: viewModel.isEditing,
                            beforeCursor: digit - 1 < viewModel.otpField.count,
                            afterCursor: viewModel.otpField.count < digit - 1
                        )
                    }
                } //: HSTACK
                
                // TEXTFIELD FOR EDITING
                TextField("", text: $viewModel.otpField) { isEditing in
                    viewModel.isEditing = isEditing
                }
                .font(Font.system(size: 90, design: .default))
                .offset(x: 12, y: 10)
                .frame(width: viewModel.isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                .textContentType(.oneTimeCode)
                .foregroundColor(.clear)
                .background(Color.clear)
                .keyboardType(.numberPad)
                .accentColor(.clear)
                
                // PASTE BUTTON
                Button(action: pasteText, label: {
                    Text("Paste")
                })
                .padding(.top, 9)
                .padding(.bottom, 9)
                .padding(.trailing, 16)
                .padding(.leading, 16)
                .font(Font.system(size: 14, design: .default))
                .accentColor(Color(.white))
                .background(Color(colorScheme == .light ? UIColor.black : UIColor.systemGray6))
                .cornerRadius(7.0)
                .overlay(
                    RoundedRectangle(cornerRadius: 7).stroke(Color(.black), lineWidth: 2)
                )
                .opacity(viewModel.showPasteButton ? 1 : 0)
                .offset(x: viewModel.numberOfFields == 6 ? -150 : -100, y: -40)
            } //: ZSTACK
        } //: VSTACK
    }
    
    func pasteText() {
        let pasteboard = UIPasteboard.general
        guard let pastedString = pasteboard.string else {
            return
        }
        let decimalInputOnly = pastedString
            .components(separatedBy:CharacterSet.decimalDigits.inverted)
            .joined()
        let otpField = decimalInputOnly.prefix(viewModel.numberOfFields)
        viewModel.otpField = String(otpField)
    }
    
    @available(iOS 13.0, *)
    private func otpText(
        text: String,
        isEditing: Bool,
        beforeCursor: Bool,
        afterCursor: Bool
    ) -> some View {
        return Text(text)
            .font(Font.custom("GTWalsheim-Regular", size: 34))
            .frame(width: textBoxWidth, height: textBoxHeight)
            .background(VStack{
                Spacer()
                    .frame(height: 65)
                ZStack {
                    Capsule()
                        .frame(width: textBoxWidth, height: 2)
                        .foregroundColor(Color(hex: "#BCBEC0"))
                    
                    Capsule()
                        .frame(width: textBoxWidth, height: 2)
                        .foregroundColor(Color(hex: "#367878"))
                        .offset(x: (beforeCursor ? textBoxWidth : 0) + (afterCursor ? -textBoxWidth : 0))
                        .animation(.easeInOut, value: [beforeCursor, afterCursor])
                    .opacity(isEditing ? 1 : 0)
                } //: ZSTACK
                .clipped()
            })
            .padding(paddingOfBox)
    }
}

@available(iOS 13.0.0, *)
struct CXLRPOTPView_Previews: PreviewProvider {
    static var previews: some View {
        CXLRPOTPView(viewModel: OTPViewModel())
            .previewLayout(.sizeThatFits)
    }
}

看来你在 otp 上遇到了困难。 :)

我通过添加两个变量更新了 OTPViewModel

一个 OperationQueuemaxConcurrentOperationCount 设置为一个允许数字被一个接一个地添加到 otpField。 (修复动画)

一个字符串 userPastedTextdidSet 属性 观察者,我在其中一个一个地添加到 otpField

我还将 paste 函数从 CXLRPOTPView 更新为“粘贴”在 userPastedText 中。这是代码。

class OTPViewModel: ObservableObject {

    let operationQueue: OperationQueue = {
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        return operationQueue
    }()

.
.
.

    var userPastedText = "" {
        didSet {
            for char in userPastedText {
                operationQueue.addOperation {
                    Thread.sleep(forTimeInterval: 0.2)
                    DispatchQueue.main.async {
                        self.otpField += String(char)
                    }
                }
                
            }
        }
    }

.
.
.

}
struct CXLRPOTPView: View {
.
.
.
    func pasteText() {
        let pasteboard = UIPasteboard.general
        guard let pastedString = pasteboard.string else {
            return
        }
        let decimalInputOnly = pastedString
            .components(separatedBy:CharacterSet.decimalDigits.inverted)
            .joined()
        let otpField = decimalInputOnly.prefix(viewModel.numberOfFields)
        viewModel.userPastedText = String(otpField)
    }
.
.
.

}